Create account sticker editor
This commit is contained in:
@@ -59,4 +59,4 @@ Dependencies:
|
|||||||
'frameworks/kdbusaddons': '@latest-kf6'
|
'frameworks/kdbusaddons': '@latest-kf6'
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows/Qt5' ]
|
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD' ]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ add_library(neochat STATIC
|
|||||||
models/messagefiltermodel.cpp
|
models/messagefiltermodel.cpp
|
||||||
models/roomlistmodel.cpp
|
models/roomlistmodel.cpp
|
||||||
models/sortfilterspacelistmodel.cpp
|
models/sortfilterspacelistmodel.cpp
|
||||||
|
models/accountstickermodel.cpp
|
||||||
spacehierarchycache.cpp
|
spacehierarchycache.cpp
|
||||||
roommanager.cpp
|
roommanager.cpp
|
||||||
neochatroom.cpp
|
neochatroom.cpp
|
||||||
@@ -24,6 +25,7 @@ add_library(neochat STATIC
|
|||||||
models/publicroomlistmodel.cpp
|
models/publicroomlistmodel.cpp
|
||||||
models/userdirectorylistmodel.cpp
|
models/userdirectorylistmodel.cpp
|
||||||
models/keywordnotificationrulemodel.cpp
|
models/keywordnotificationrulemodel.cpp
|
||||||
|
models/emoticonfiltermodel.cpp
|
||||||
notificationsmanager.cpp
|
notificationsmanager.cpp
|
||||||
models/sortfilterroomlistmodel.cpp
|
models/sortfilterroomlistmodel.cpp
|
||||||
chatdocumenthandler.cpp
|
chatdocumenthandler.cpp
|
||||||
|
|||||||
@@ -51,5 +51,42 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
|
|||||||
|
|
||||||
void ImagePackEventContent::fillJson(QJsonObject *o) const
|
void ImagePackEventContent::fillJson(QJsonObject *o) const
|
||||||
{
|
{
|
||||||
// TODO
|
if (pack) {
|
||||||
|
QJsonObject packJson;
|
||||||
|
if (pack->displayName) {
|
||||||
|
packJson["display_name"] = *pack->displayName;
|
||||||
|
}
|
||||||
|
if (pack->usage) {
|
||||||
|
QJsonArray usageJson;
|
||||||
|
for (const auto &usage : *pack->usage) {
|
||||||
|
usageJson += usage;
|
||||||
|
}
|
||||||
|
packJson["usage"] = usageJson;
|
||||||
|
}
|
||||||
|
if (pack->avatarUrl) {
|
||||||
|
packJson["avatar_url"] = pack->avatarUrl->toString();
|
||||||
|
}
|
||||||
|
if (pack->attribution) {
|
||||||
|
packJson["attribution"] = *pack->attribution;
|
||||||
|
}
|
||||||
|
(*o)["pack"_ls] = packJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject imagesJson;
|
||||||
|
for (const auto &image : images) {
|
||||||
|
QJsonObject imageJson;
|
||||||
|
imageJson["url"] = image.url.toString();
|
||||||
|
if (image.body) {
|
||||||
|
imageJson["body"] = *image.body;
|
||||||
|
}
|
||||||
|
if (image.usage) {
|
||||||
|
QJsonArray usageJson;
|
||||||
|
for (const auto &usage : *image.usage) {
|
||||||
|
usageJson += usage;
|
||||||
|
}
|
||||||
|
imageJson["usage"] = usageJson;
|
||||||
|
}
|
||||||
|
imagesJson[image.shortcode] = imageJson;
|
||||||
|
}
|
||||||
|
(*o)["images"_ls] = imagesJson;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,12 @@
|
|||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "login.h"
|
#include "login.h"
|
||||||
#include "matriximageprovider.h"
|
#include "matriximageprovider.h"
|
||||||
|
#include "models/accountstickermodel.h"
|
||||||
#include "models/collapsestateproxymodel.h"
|
#include "models/collapsestateproxymodel.h"
|
||||||
#include "models/customemojimodel.h"
|
#include "models/customemojimodel.h"
|
||||||
#include "models/devicesmodel.h"
|
#include "models/devicesmodel.h"
|
||||||
#include "models/emojimodel.h"
|
#include "models/emojimodel.h"
|
||||||
|
#include "models/emoticonfiltermodel.h"
|
||||||
#include "models/imagepacksmodel.h"
|
#include "models/imagepacksmodel.h"
|
||||||
#include "models/keywordnotificationrulemodel.h"
|
#include "models/keywordnotificationrulemodel.h"
|
||||||
#include "models/messageeventmodel.h"
|
#include "models/messageeventmodel.h"
|
||||||
@@ -248,6 +250,8 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<KeywordNotificationRuleModel>("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel");
|
qmlRegisterType<KeywordNotificationRuleModel>("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel");
|
||||||
qmlRegisterType<StickerModel>("org.kde.neochat", 1, 0, "StickerModel");
|
qmlRegisterType<StickerModel>("org.kde.neochat", 1, 0, "StickerModel");
|
||||||
qmlRegisterType<ImagePacksModel>("org.kde.neochat", 1, 0, "ImagePacksModel");
|
qmlRegisterType<ImagePacksModel>("org.kde.neochat", 1, 0, "ImagePacksModel");
|
||||||
|
qmlRegisterType<AccountStickerModel>("org.kde.neochat", 1, 0, "AccountStickerModel");
|
||||||
|
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
|
||||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
||||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
|
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
|
||||||
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM");
|
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM");
|
||||||
|
|||||||
178
src/models/accountstickermodel.cpp
Normal file
178
src/models/accountstickermodel.cpp
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "accountstickermodel.h"
|
||||||
|
|
||||||
|
#include <csapi/content-repo.h>
|
||||||
|
#include <qcoro/qcorosignal.h>
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
AccountStickerModel::AccountStickerModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int AccountStickerModel::rowCount(const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(index);
|
||||||
|
if (!m_images) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return m_images->images.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant AccountStickerModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
const auto &row = index.row();
|
||||||
|
const auto &image = m_images->images[row];
|
||||||
|
if (role == UrlRole) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
return m_connection->makeMediaUrl(image.url);
|
||||||
|
#else
|
||||||
|
return QUrl();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (role == BodyRole) {
|
||||||
|
if (image.body) {
|
||||||
|
return *image.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (role == ShortCodeRole) {
|
||||||
|
return image.shortcode;
|
||||||
|
}
|
||||||
|
if (role == IsStickerRole) {
|
||||||
|
if (image.usage) {
|
||||||
|
return image.usage->isEmpty() || image.usage->contains("sticker"_ls);
|
||||||
|
}
|
||||||
|
if (m_images->pack && m_images->pack->usage) {
|
||||||
|
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("sticker"_ls);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (role == IsEmojiRole) {
|
||||||
|
if (image.usage) {
|
||||||
|
return image.usage->isEmpty() || image.usage->contains("emoticon"_ls);
|
||||||
|
}
|
||||||
|
if (m_images->pack && m_images->pack->usage) {
|
||||||
|
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("emoticon"_ls);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> AccountStickerModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{AccountStickerModel::UrlRole, "url"},
|
||||||
|
{AccountStickerModel::BodyRole, "body"},
|
||||||
|
{AccountStickerModel::ShortCodeRole, "shortcode"},
|
||||||
|
{AccountStickerModel::IsStickerRole, "isSticker"},
|
||||||
|
{AccountStickerModel::IsEmojiRole, "isEmoji"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection *AccountStickerModel::connection() const
|
||||||
|
{
|
||||||
|
return m_connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountStickerModel::setConnection(Connection *connection)
|
||||||
|
{
|
||||||
|
if (m_connection) {
|
||||||
|
disconnect(m_connection, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_connection = connection;
|
||||||
|
Q_EMIT connectionChanged();
|
||||||
|
connect(m_connection, &Connection::accountDataChanged, this, [this](QString type) {
|
||||||
|
if (type == QStringLiteral("im.ponies.user_emotes")) {
|
||||||
|
reloadStickers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reloadStickers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountStickerModel::reloadStickers()
|
||||||
|
{
|
||||||
|
if (!m_connection->hasAccountData("im.ponies.user_emotes"_ls)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto json = m_connection->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
||||||
|
const auto &content = ImagePackEventContent(json);
|
||||||
|
beginResetModel();
|
||||||
|
m_images = content;
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountStickerModel::deleteSticker(int index)
|
||||||
|
{
|
||||||
|
QJsonObject data;
|
||||||
|
m_images->images.removeAt(index);
|
||||||
|
m_images->fillJson(&data);
|
||||||
|
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountStickerModel::setStickerBody(int index, const QString &text)
|
||||||
|
{
|
||||||
|
m_images->images[index].body = text;
|
||||||
|
QJsonObject data;
|
||||||
|
m_images->fillJson(&data);
|
||||||
|
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountStickerModel::setStickerShortcode(int index, const QString &shortcode)
|
||||||
|
{
|
||||||
|
m_images->images[index].shortcode = shortcode;
|
||||||
|
QJsonObject data;
|
||||||
|
m_images->fillJson(&data);
|
||||||
|
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountStickerModel::setStickerImage(int index, const QUrl &source)
|
||||||
|
{
|
||||||
|
doSetStickerImage(index, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> AccountStickerModel::doSetStickerImage(int index, QUrl source)
|
||||||
|
{
|
||||||
|
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
||||||
|
co_await qCoro(job, &BaseJob::finished);
|
||||||
|
if (job->error() != BaseJob::NoError) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
m_images->images[index].url = job->contentUri().toString();
|
||||||
|
#else
|
||||||
|
m_images->images[index].url = job->contentUri();
|
||||||
|
#endif
|
||||||
|
m_images->images[index].info = none;
|
||||||
|
QJsonObject data;
|
||||||
|
m_images->fillJson(&data);
|
||||||
|
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> AccountStickerModel::doAddSticker(QUrl source, QString shortcode, QString description)
|
||||||
|
{
|
||||||
|
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
||||||
|
co_await qCoro(job, &BaseJob::finished);
|
||||||
|
if (job->error() != BaseJob::NoError) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
m_images->images.append(ImagePackEventContent::ImagePackImage{
|
||||||
|
shortcode,
|
||||||
|
job->contentUri(),
|
||||||
|
description,
|
||||||
|
none,
|
||||||
|
QStringList{"sticker"_ls},
|
||||||
|
});
|
||||||
|
QJsonObject data;
|
||||||
|
m_images->fillJson(&data);
|
||||||
|
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountStickerModel::addSticker(const QUrl &source, const QString &shortcode, const QString &description)
|
||||||
|
{
|
||||||
|
doAddSticker(source, shortcode, description);
|
||||||
|
}
|
||||||
101
src/models/accountstickermodel.h
Normal file
101
src/models/accountstickermodel.h
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "events/imagepackevent.h"
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QCoroTask>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QVector>
|
||||||
|
#include <connection.h>
|
||||||
|
|
||||||
|
class ImagePacksModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class AccountStickerModel
|
||||||
|
*
|
||||||
|
* This class defines the model for visualising the account stickers.
|
||||||
|
*
|
||||||
|
* This is based upon the im.ponies.user_emotes spec (MSC2545).
|
||||||
|
*/
|
||||||
|
class AccountStickerModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
/**
|
||||||
|
* @brief The connection to get stickers from.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
UrlRole = Qt::UserRole + 1, /**< The URL for the sticker. */
|
||||||
|
ShortCodeRole, /**< The shortcode for the sticker. */
|
||||||
|
BodyRole, //**< A textual description of the sticker */
|
||||||
|
IsStickerRole, //**< Whether this emoticon is a sticker */
|
||||||
|
IsEmojiRole, //**< Whether this emoticon is an emoji */
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit AccountStickerModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of rows in the model.
|
||||||
|
*
|
||||||
|
* @sa QAbstractItemModel::rowCount
|
||||||
|
*/
|
||||||
|
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the given role value at the given index.
|
||||||
|
*
|
||||||
|
* @sa QAbstractItemModel::data
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns a mapping from Role enum values to role names.
|
||||||
|
*
|
||||||
|
* @sa Roles, QAbstractItemModel::roleNames()
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] Quotient::Connection *connection() const;
|
||||||
|
void setConnection(Quotient::Connection *connection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deletes the sticker at the given index.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void deleteSticker(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Changes the description for the sticker at the given index.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void setStickerBody(int index, const QString &text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Changes the shortcode for the sticker at the given index.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void setStickerShortcode(int index, const QString &shortCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Changes the image for the sticker at the given index.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void setStickerImage(int index, const QUrl &source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Adds a sticker with the given parameters.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void addSticker(const QUrl &source, const QString &shortcode, const QString &description);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void connectionChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<Quotient::ImagePackEventContent> m_images;
|
||||||
|
QPointer<Quotient::Connection> m_connection;
|
||||||
|
QCoro::Task<void> doSetStickerImage(int index, QUrl source);
|
||||||
|
QCoro::Task<void> doAddSticker(QUrl source, QString shortcode, QString description);
|
||||||
|
|
||||||
|
void reloadStickers();
|
||||||
|
};
|
||||||
@@ -86,6 +86,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
|||||||
{QStringLiteral("url"), url},
|
{QStringLiteral("url"), url},
|
||||||
{QStringLiteral("info"), imageInfo},
|
{QStringLiteral("info"), imageInfo},
|
||||||
{QStringLiteral("body"), location.fileName()},
|
{QStringLiteral("body"), location.fileName()},
|
||||||
|
{"usage"_ls, "emoticon"_ls},
|
||||||
});
|
});
|
||||||
|
|
||||||
json["images"] = emojiData;
|
json["images"] = emojiData;
|
||||||
|
|||||||
40
src/models/emoticonfiltermodel.cpp
Normal file
40
src/models/emoticonfiltermodel.cpp
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "emoticonfiltermodel.h"
|
||||||
|
|
||||||
|
#include "accountstickermodel.h"
|
||||||
|
|
||||||
|
EmoticonFilterModel::EmoticonFilterModel(QObject *parent)
|
||||||
|
: QSortFilterProxyModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmoticonFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||||
|
{
|
||||||
|
auto stickerUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), AccountStickerModel::IsStickerRole).toBool();
|
||||||
|
auto emojiUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), AccountStickerModel::IsEmojiRole).toBool();
|
||||||
|
return (stickerUsage && m_showStickers) || (emojiUsage && m_showEmojis);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmoticonFilterModel::showStickers() const
|
||||||
|
{
|
||||||
|
return m_showStickers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmoticonFilterModel::setShowStickers(bool showStickers)
|
||||||
|
{
|
||||||
|
m_showStickers = showStickers;
|
||||||
|
Q_EMIT showStickersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmoticonFilterModel::showEmojis() const
|
||||||
|
{
|
||||||
|
return m_showEmojis;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmoticonFilterModel::setShowEmojis(bool showEmojis)
|
||||||
|
{
|
||||||
|
m_showEmojis = showEmojis;
|
||||||
|
Q_EMIT showEmojisChanged();
|
||||||
|
}
|
||||||
49
src/models/emoticonfiltermodel.h
Normal file
49
src/models/emoticonfiltermodel.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class EmoticonFilterModel
|
||||||
|
*
|
||||||
|
* This class creates a custom QSortFilterProxyModel for filtering a emoticon by type
|
||||||
|
* (Sticker or Emoji).
|
||||||
|
*/
|
||||||
|
class EmoticonFilterModel : public QSortFilterProxyModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether stickers should be shown
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether emojis show be shown
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool showEmojis READ showEmojis WRITE setShowEmojis NOTIFY showEmojisChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit EmoticonFilterModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Custom filter function checking the type of emoticon
|
||||||
|
*
|
||||||
|
* @note The filter cannot be modified and will always use the same filter properties.
|
||||||
|
*/
|
||||||
|
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] bool showStickers() const;
|
||||||
|
void setShowStickers(bool showStickers);
|
||||||
|
|
||||||
|
[[nodiscard]] bool showEmojis() const;
|
||||||
|
void setShowEmojis(bool showEmojis);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void showStickersChanged();
|
||||||
|
void showEmojisChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_showStickers = false;
|
||||||
|
bool m_showEmojis = false;
|
||||||
|
};
|
||||||
@@ -20,7 +20,11 @@ int ImagePacksModel::rowCount(const QModelIndex &index) const
|
|||||||
|
|
||||||
QVariant ImagePacksModel::data(const QModelIndex &index, int role) const
|
QVariant ImagePacksModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
const auto &event = m_events[index.row()];
|
const auto row = index.row();
|
||||||
|
if (row < 0 || row >= m_events.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto &event = m_events[row];
|
||||||
if (role == DisplayNameRole) {
|
if (role == DisplayNameRole) {
|
||||||
if (event.pack->displayName) {
|
if (event.pack->displayName) {
|
||||||
return *event.pack->displayName;
|
return *event.pack->displayName;
|
||||||
@@ -59,14 +63,24 @@ void ImagePacksModel::setRoom(NeoChatRoom *room)
|
|||||||
{
|
{
|
||||||
if (m_room) {
|
if (m_room) {
|
||||||
disconnect(m_room, nullptr, this, nullptr);
|
disconnect(m_room, nullptr, this, nullptr);
|
||||||
|
disconnect(m_room->connection(), nullptr, this, nullptr);
|
||||||
}
|
}
|
||||||
m_room = room;
|
m_room = room;
|
||||||
|
|
||||||
|
connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) {
|
||||||
|
if (type == "im.ponies.user_emotes"_ls) {
|
||||||
|
reloadImages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// TODO listen to packs changing
|
||||||
|
reloadImages();
|
||||||
|
Q_EMIT roomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImagePacksModel::reloadImages()
|
||||||
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_events.clear();
|
m_events.clear();
|
||||||
|
|
||||||
// TODO listen to account data changing
|
|
||||||
// TODO listen to packs changing
|
|
||||||
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
|
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
|
||||||
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
||||||
json["pack"] = QJsonObject{
|
json["pack"] = QJsonObject{
|
||||||
@@ -110,8 +124,8 @@ void ImagePacksModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
Q_EMIT imagesLoaded();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
Q_EMIT roomChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ImagePacksModel::showStickers() const
|
bool ImagePacksModel::showStickers() const
|
||||||
|
|||||||
@@ -90,10 +90,12 @@ Q_SIGNALS:
|
|||||||
void roomChanged();
|
void roomChanged();
|
||||||
void showStickersChanged();
|
void showStickersChanged();
|
||||||
void showEmoticonsChanged();
|
void showEmoticonsChanged();
|
||||||
|
void imagesLoaded();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<NeoChatRoom> m_room;
|
QPointer<NeoChatRoom> m_room;
|
||||||
QVector<Quotient::ImagePackEventContent> m_events;
|
QVector<Quotient::ImagePackEventContent> m_events;
|
||||||
bool m_showStickers = true;
|
bool m_showStickers = true;
|
||||||
bool m_showEmoticons = true;
|
bool m_showEmoticons = true;
|
||||||
|
void reloadImages();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,14 +51,11 @@ void StickerModel::setModel(ImagePacksModel *model)
|
|||||||
disconnect(m_model, nullptr, this, nullptr);
|
disconnect(m_model, nullptr, this, nullptr);
|
||||||
}
|
}
|
||||||
connect(model, &ImagePacksModel::roomChanged, this, [this]() {
|
connect(model, &ImagePacksModel::roomChanged, this, [this]() {
|
||||||
beginResetModel();
|
reloadImages();
|
||||||
m_images = m_model->images(m_index);
|
|
||||||
endResetModel();
|
|
||||||
});
|
});
|
||||||
beginResetModel();
|
connect(model, &ImagePacksModel::imagesLoaded, this, &StickerModel::reloadImages);
|
||||||
m_model = model;
|
m_model = model;
|
||||||
m_images = m_model->images(m_index);
|
reloadImages();
|
||||||
endResetModel();
|
|
||||||
Q_EMIT modelChanged();
|
Q_EMIT modelChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +65,18 @@ int StickerModel::packIndex() const
|
|||||||
}
|
}
|
||||||
void StickerModel::setPackIndex(int index)
|
void StickerModel::setPackIndex(int index)
|
||||||
{
|
{
|
||||||
beginResetModel();
|
|
||||||
m_index = index;
|
m_index = index;
|
||||||
|
Q_EMIT packIndexChanged();
|
||||||
|
reloadImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerModel::reloadImages()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
if (m_model) {
|
if (m_model) {
|
||||||
m_images = m_model->images(m_index);
|
m_images = m_model->images(m_index);
|
||||||
}
|
}
|
||||||
endResetModel();
|
endResetModel();
|
||||||
Q_EMIT packIndexChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NeoChatRoom *StickerModel::room() const
|
NeoChatRoom *StickerModel::room() const
|
||||||
@@ -84,6 +86,9 @@ NeoChatRoom *StickerModel::room() const
|
|||||||
|
|
||||||
void StickerModel::setRoom(NeoChatRoom *room)
|
void StickerModel::setRoom(NeoChatRoom *room)
|
||||||
{
|
{
|
||||||
|
if (room) {
|
||||||
|
disconnect(room->connection(), nullptr, this, nullptr);
|
||||||
|
}
|
||||||
m_room = room;
|
m_room = room;
|
||||||
Q_EMIT roomChanged();
|
Q_EMIT roomChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,4 +98,5 @@ private:
|
|||||||
int m_index = 0;
|
int m_index = 0;
|
||||||
QVector<Quotient::ImagePackEventContent::ImagePackImage> m_images;
|
QVector<Quotient::ImagePackEventContent::ImagePackImage> m_images;
|
||||||
NeoChatRoom *m_room;
|
NeoChatRoom *m_room;
|
||||||
|
void reloadImages();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ Kirigami.CategorizedSettings {
|
|||||||
icon.name: "preferences-desktop-emoticons"
|
icon.name: "preferences-desktop-emoticons"
|
||||||
page: Qt.resolvedUrl("Emoticons.qml")
|
page: Qt.resolvedUrl("Emoticons.qml")
|
||||||
},
|
},
|
||||||
|
Kirigami.SettingAction {
|
||||||
|
actionName: "stickers"
|
||||||
|
text: i18n("Stickers")
|
||||||
|
icon.name: "stickers"
|
||||||
|
page: Qt.resolvedUrl("StickersPage.qml")
|
||||||
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
actionName: "spellChecking"
|
actionName: "spellChecking"
|
||||||
text: i18n("Spell Checking")
|
text: i18n("Spell Checking")
|
||||||
|
|||||||
148
src/qml/Settings/StickerEditorPage.qml
Normal file
148
src/qml/Settings/StickerEditorPage.qml
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
import Qt.labs.platform 1.1
|
||||||
|
import QtQuick.Window 2.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.19 as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
Kirigami.ScrollablePage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property string description
|
||||||
|
required property string index
|
||||||
|
required property string url
|
||||||
|
required property string shortcode
|
||||||
|
required property var model
|
||||||
|
required property var proxyModel
|
||||||
|
property bool newSticker: false
|
||||||
|
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
|
||||||
|
title: newSticker ? i18nc("@title", "Add Sticker") : i18nc("@title", "Edit Sticker")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
MobileForm.FormCard {
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.fillWidth: true
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
MobileForm.FormCardHeader {
|
||||||
|
title: i18n("Sticker")
|
||||||
|
}
|
||||||
|
MobileForm.AbstractFormDelegate {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
background: Item {}
|
||||||
|
contentItem: RowLayout {
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
id: image
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
source: root.url
|
||||||
|
sourceSize.width: Kirigami.Units.gridUnit * 4
|
||||||
|
sourceSize.height: Kirigami.Units.gridUnit * 4
|
||||||
|
width: Kirigami.Units.gridUnit * 4
|
||||||
|
height: Kirigami.Units.gridUnit * 4
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: "stickers"
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: parent.status !== Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "edit-entry"
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
onClicked: mouseArea.clicked()
|
||||||
|
text: image.source != "" ? i18n("Change Image") : i18n("Set Image")
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
display: QQC2.Button.IconOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
property var fileDialog: null;
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (fileDialog != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.Overlay)
|
||||||
|
fileDialog.chosen.connect(function(receivedSource) {
|
||||||
|
mouseArea.fileDialog = null;
|
||||||
|
if (!receivedSource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent.source = receivedSource;
|
||||||
|
});
|
||||||
|
fileDialog.onRejected.connect(function() {
|
||||||
|
mouseArea.fileDialog = null;
|
||||||
|
});
|
||||||
|
fileDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MobileForm.FormTextFieldDelegate {
|
||||||
|
id: shortcode
|
||||||
|
label: i18n("Shortcode:")
|
||||||
|
text: root.shortcode
|
||||||
|
}
|
||||||
|
MobileForm.FormTextFieldDelegate {
|
||||||
|
id: description
|
||||||
|
label: i18n("Description:")
|
||||||
|
text: root.description
|
||||||
|
}
|
||||||
|
MobileForm.FormButtonDelegate {
|
||||||
|
id: save
|
||||||
|
text: i18n("Save")
|
||||||
|
icon.name: "document-save"
|
||||||
|
enabled: !root.newSticker || (image.source && shortcode.text && description.text)
|
||||||
|
onClicked: {
|
||||||
|
if (root.newSticker) {
|
||||||
|
model.addSticker(image.source, shortcode.text, description.text)
|
||||||
|
} else {
|
||||||
|
if (description.text !== root.description) {
|
||||||
|
root.model.setStickerBody(proxyModel.mapToSource(proxyModel.index(model.index, 0)).row, description.text)
|
||||||
|
}
|
||||||
|
if (shortcode.text !== root.shortcode) {
|
||||||
|
root.model.setStickerShortcode(proxyModel.mapToSource(proxyModel.index(model.index, 0)).row, shortcode.text)
|
||||||
|
}
|
||||||
|
if (image.source + "" !== root.url) {
|
||||||
|
root.model.setStickerImage(proxyModel.mapToSource(proxyModel.index(model.index, 0)).row, image.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.closeDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component {
|
||||||
|
id: openFileDialog
|
||||||
|
|
||||||
|
OpenFileDialog {
|
||||||
|
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||||
|
parentWindow: root.Window.window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/qml/Settings/StickersPage.qml
Normal file
128
src/qml/Settings/StickersPage.qml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-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.19 as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
Kirigami.ScrollablePage {
|
||||||
|
title: i18n("Stickers")
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
MobileForm.FormCard {
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.fillWidth: true
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
MobileForm.FormCardHeader {
|
||||||
|
title: i18n("Stickers")
|
||||||
|
}
|
||||||
|
Flow {
|
||||||
|
id: stickerFlow
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Repeater {
|
||||||
|
model: EmoticonFilterModel {
|
||||||
|
id: emoticonFilterModel
|
||||||
|
sourceModel: AccountStickerModel {
|
||||||
|
id: stickerModel
|
||||||
|
connection: Controller.activeConnection
|
||||||
|
}
|
||||||
|
showStickers: true
|
||||||
|
showEmojis: false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: MobileForm.AbstractFormDelegate {
|
||||||
|
id: stickerDelegate
|
||||||
|
|
||||||
|
width: stickerFlow.width / 4
|
||||||
|
height: width
|
||||||
|
|
||||||
|
onClicked: pageSettingStack.pushDialogLayer(stickerEditorPage, {
|
||||||
|
description: model.body ?? "",
|
||||||
|
index: model.index,
|
||||||
|
url: model.url,
|
||||||
|
shortcode: model.shortcode,
|
||||||
|
model: stickerModel,
|
||||||
|
proxyModel: emoticonFilterModel
|
||||||
|
}, {
|
||||||
|
title: i18nc("@title", "Edit Sticker")
|
||||||
|
});
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
Image {
|
||||||
|
source: model.url
|
||||||
|
Layout.fillWidth: true
|
||||||
|
sourceSize.height: parent.width * 0.8
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
autoTransform: true
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: "stickers"
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: parent.status !== Image.Ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
id: descriptionLabel
|
||||||
|
text: model.body ?? i18nc("As in 'This sticker has no description'", "No Description")
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
maximumLineCount: 2
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "edit-delete"
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
z: 2
|
||||||
|
onClicked: stickerModel.deleteSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(model.index, 0)).row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MobileForm.AbstractFormDelegate {
|
||||||
|
width: stickerFlow.width / 4
|
||||||
|
height: width
|
||||||
|
|
||||||
|
onClicked: pageSettingStack.pushDialogLayer(stickerEditorPage, {
|
||||||
|
description: "",
|
||||||
|
index: -1,
|
||||||
|
url: "",
|
||||||
|
shortcode: "",
|
||||||
|
model: stickerModel,
|
||||||
|
proxyModel: emoticonFilterModel,
|
||||||
|
newSticker: true
|
||||||
|
}, {
|
||||||
|
title: i18nc("@title", "Add Sticker")
|
||||||
|
});
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: "list-add"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18n("Add Sticker")
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: stickerEditorPage
|
||||||
|
StickerEditorPage {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,6 +98,8 @@
|
|||||||
<file alias="ColorScheme.qml">qml/Settings/ColorScheme.qml</file>
|
<file alias="ColorScheme.qml">qml/Settings/ColorScheme.qml</file>
|
||||||
<file alias="GeneralSettingsPage.qml">qml/Settings/GeneralSettingsPage.qml</file>
|
<file alias="GeneralSettingsPage.qml">qml/Settings/GeneralSettingsPage.qml</file>
|
||||||
<file alias="Emoticons.qml">qml/Settings/Emoticons.qml</file>
|
<file alias="Emoticons.qml">qml/Settings/Emoticons.qml</file>
|
||||||
|
<file alias="StickersPage.qml">qml/Settings/StickersPage.qml</file>
|
||||||
|
<file alias="StickerEditorPage.qml">qml/Settings/StickerEditorPage.qml</file>
|
||||||
<file alias="GlobalNotificationsPage.qml">qml/Settings/GlobalNotificationsPage.qml</file>
|
<file alias="GlobalNotificationsPage.qml">qml/Settings/GlobalNotificationsPage.qml</file>
|
||||||
<file alias="NotificationRuleItem.qml">qml/Settings/NotificationRuleItem.qml</file>
|
<file alias="NotificationRuleItem.qml">qml/Settings/NotificationRuleItem.qml</file>
|
||||||
<file alias="AppearanceSettingsPage.qml">qml/Settings/AppearanceSettingsPage.qml</file>
|
<file alias="AppearanceSettingsPage.qml">qml/Settings/AppearanceSettingsPage.qml</file>
|
||||||
|
|||||||
Reference in New Issue
Block a user