Implement sending stickers
MSC2545 image packs are used as source.
This commit is contained in:
@@ -49,6 +49,9 @@ add_library(neochat STATIC
|
|||||||
models/searchmodel.cpp
|
models/searchmodel.cpp
|
||||||
texthandler.cpp
|
texthandler.cpp
|
||||||
logger.cpp
|
logger.cpp
|
||||||
|
imagepackevent.cpp
|
||||||
|
stickermodel.cpp
|
||||||
|
imagepacksmodel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(neochat-app
|
add_executable(neochat-app
|
||||||
|
|||||||
54
src/imagepackevent.cpp
Normal file
54
src/imagepackevent.cpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "imagepackevent.h"
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
|
||||||
|
{
|
||||||
|
if(json.contains(QStringLiteral("pack"))) {
|
||||||
|
pack = ImagePackEventContent::Pack{
|
||||||
|
fromJson<Omittable<QString>>(json["pack"].toObject()["display_name"]),
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
fromJson<Omittable<QUrl>>(json["pack"].toObject()["avatar_url"]),
|
||||||
|
#else
|
||||||
|
QUrl(),
|
||||||
|
#endif
|
||||||
|
fromJson<Omittable<QStringList>>(json["pack"].toObject()["usage"]),
|
||||||
|
fromJson<Omittable<QString>>(json["pack"].toObject()["attribution"]),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
pack = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &keys = json["images"].toObject().keys();
|
||||||
|
for (const auto &k : keys) {
|
||||||
|
Omittable<EventContent::ImageInfo> info;
|
||||||
|
if (json["images"][k].toObject().contains(QStringLiteral("info"))) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
info = EventContent::ImageInfo(QUrl(json["images"][k]["url"].toString()), json["images"][k]["info"].toObject(), k);
|
||||||
|
#else
|
||||||
|
info = EventContent::ImageInfo(QUrl(json["images"][k]["url"].toString()), json["images"][k].toObject(), k);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
info = none;
|
||||||
|
}
|
||||||
|
images += ImagePackImage{
|
||||||
|
k,
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
fromJson<QUrl>(json["images"][k]["url"].toString()),
|
||||||
|
#else
|
||||||
|
QUrl(),
|
||||||
|
#endif
|
||||||
|
fromJson<Omittable<QString>>(json["images"][k]["body"]),
|
||||||
|
info,
|
||||||
|
fromJson<Omittable<QStringList>>(json["images"][k]["usage"]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImagePackEventContent::fillJson(QJsonObject* o) const {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
58
src/imagepackevent.h
Normal file
58
src/imagepackevent.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QVector>
|
||||||
|
#include <events/eventcontent.h>
|
||||||
|
#include <events/stateevent.h>
|
||||||
|
|
||||||
|
namespace Quotient
|
||||||
|
{
|
||||||
|
class ImagePackEventContent
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Pack {
|
||||||
|
Quotient::Omittable<QString> displayName;
|
||||||
|
Quotient::Omittable<QUrl> avatarUrl;
|
||||||
|
Quotient::Omittable<QStringList> usage;
|
||||||
|
Quotient::Omittable<QString> attribution;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImagePackImage {
|
||||||
|
QString shortcode;
|
||||||
|
QUrl url;
|
||||||
|
Quotient::Omittable<QString> body;
|
||||||
|
Quotient::Omittable<Quotient::EventContent::ImageInfo> info;
|
||||||
|
Quotient::Omittable<QStringList> usage;
|
||||||
|
};
|
||||||
|
|
||||||
|
Quotient::Omittable<Pack> pack;
|
||||||
|
QVector<ImagePackEventContent::ImagePackImage> images;
|
||||||
|
|
||||||
|
explicit ImagePackEventContent(const QJsonObject &o);
|
||||||
|
|
||||||
|
void fillJson(QJsonObject *o) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
class ImagePackEvent : public KeyedStateEventBase<ImagePackEvent, ImagePackEventContent>
|
||||||
|
#else
|
||||||
|
class ImagePackEvent : public StateEvent<ImagePackEventContent>
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
QUO_EVENT(ImagePackEvent, "im.ponies.room_emotes")
|
||||||
|
using KeyedStateEventBase::KeyedStateEventBase;
|
||||||
|
#else
|
||||||
|
DEFINE_EVENT_TYPEID("im.ponies.room_emotes", ImagePackEvent)
|
||||||
|
explicit ImagePackEvent(const QJsonObject &obj)
|
||||||
|
: StateEvent(typeId(), obj)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
REGISTER_EVENT_TYPE(ImagePackEvent)
|
||||||
|
}
|
||||||
135
src/imagepacksmodel.cpp
Normal file
135
src/imagepacksmodel.cpp
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "imagepacksmodel.h"
|
||||||
|
#include "imagepackevent.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
ImagePacksModel::ImagePacksModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int ImagePacksModel::rowCount(const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
return m_events.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ImagePacksModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
const auto &event = m_events[index.row()];
|
||||||
|
if (role == DisplayNameRole) {
|
||||||
|
if (event.pack->displayName) {
|
||||||
|
return *event.pack->displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (role == AvatarUrlRole) {
|
||||||
|
if (event.pack->avatarUrl) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
return m_room->connection()->makeMediaUrl(*event.pack->avatarUrl);
|
||||||
|
#endif
|
||||||
|
} else if (!event.images.empty()) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
return m_room->connection()->makeMediaUrl(event.images[0].url);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> ImagePacksModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {{DisplayNameRole, "displayName"}, {AvatarUrlRole, "avatarUrl"}, {AttributionRole, "attribution"}, {IdRole, "id"},};
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoChatRoom *ImagePacksModel::room() const
|
||||||
|
{
|
||||||
|
return m_room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImagePacksModel::setRoom(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
if (m_room) {
|
||||||
|
disconnect(m_room, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
m_room = room;
|
||||||
|
|
||||||
|
beginResetModel();
|
||||||
|
m_events.clear();
|
||||||
|
|
||||||
|
// TODO listen to account data changing
|
||||||
|
// TODO listen to packs changing
|
||||||
|
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
||||||
|
json["pack"] = QJsonObject{
|
||||||
|
{"display_name", i18n("Own Stickers")},
|
||||||
|
};
|
||||||
|
const auto &content = ImagePackEventContent(json);
|
||||||
|
if (!content.images.isEmpty()) {
|
||||||
|
m_events += ImagePackEventContent(json);
|
||||||
|
}
|
||||||
|
const auto &accountData = m_room->connection()->accountData("im.ponies.emote_rooms"_ls);
|
||||||
|
if (accountData) {
|
||||||
|
const auto &rooms = accountData->contentJson()["rooms"_ls].toObject();
|
||||||
|
for (const auto &roomId : rooms.keys()) {
|
||||||
|
if (roomId == m_room->id()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto packs = rooms[roomId].toObject();
|
||||||
|
const auto &stickerRoom = m_room->connection()->room(roomId);
|
||||||
|
for (const auto &packKey : packs.keys()) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
const auto packContent = stickerRoom->currentState().get<ImagePackEvent>(packKey)->content();
|
||||||
|
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||||
|
|| (packContent.pack->usage->contains("sticker") && showStickers())) {
|
||||||
|
m_events += packContent;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes");
|
||||||
|
for (const auto &event : events) {
|
||||||
|
auto packContent = eventCast<const ImagePackEvent>(event)->content();
|
||||||
|
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||||
|
|| (packContent.pack->usage->contains("sticker") && showStickers())) {
|
||||||
|
m_events += packContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
endResetModel();
|
||||||
|
Q_EMIT roomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImagePacksModel::showStickers() const
|
||||||
|
{
|
||||||
|
return m_showStickers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImagePacksModel::setShowStickers(bool showStickers)
|
||||||
|
{
|
||||||
|
m_showStickers = showStickers;
|
||||||
|
Q_EMIT showStickersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImagePacksModel::showEmoticons() const
|
||||||
|
{
|
||||||
|
return m_showEmoticons;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImagePacksModel::setShowEmoticons(bool showEmoticons)
|
||||||
|
{
|
||||||
|
m_showEmoticons = showEmoticons;
|
||||||
|
Q_EMIT showEmoticonsChanged();
|
||||||
|
}
|
||||||
|
QVector<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_events.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return m_events[index].images;
|
||||||
|
}
|
||||||
57
src/imagepacksmodel.h
Normal file
57
src/imagepacksmodel.h
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "imagepackevent.h"
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class NeoChatRoom;
|
||||||
|
|
||||||
|
class ImagePacksModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
|
||||||
|
Q_PROPERTY(bool showEmoticons READ showEmoticons WRITE setShowEmoticons NOTIFY showEmoticonsChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
DisplayNameRole = Qt::DisplayRole,
|
||||||
|
AvatarUrlRole,
|
||||||
|
AttributionRole,
|
||||||
|
IdRole,
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles);
|
||||||
|
|
||||||
|
explicit ImagePacksModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] NeoChatRoom *room() const;
|
||||||
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
[[nodiscard]] bool showStickers() const;
|
||||||
|
void setShowStickers(bool showStickers);
|
||||||
|
|
||||||
|
[[nodiscard]] bool showEmoticons() const;
|
||||||
|
void setShowEmoticons(bool showEmoticons);
|
||||||
|
|
||||||
|
[[nodiscard]] QVector<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void roomChanged();
|
||||||
|
void showStickersChanged();
|
||||||
|
void showEmoticonsChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
QVector<Quotient::ImagePackEventContent> m_events;
|
||||||
|
bool m_showStickers = true;
|
||||||
|
bool m_showEmoticons = true;
|
||||||
|
};
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "filetypesingleton.h"
|
#include "filetypesingleton.h"
|
||||||
|
#include "imagepacksmodel.h"
|
||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "login.h"
|
#include "login.h"
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
#include "models/statefiltermodel.h"
|
#include "models/statefiltermodel.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
#include "spacehierarchycache.h"
|
#include "spacehierarchycache.h"
|
||||||
|
#include "stickermodel.h"
|
||||||
#include "urlhelper.h"
|
#include "urlhelper.h"
|
||||||
#include "windowcontroller.h"
|
#include "windowcontroller.h"
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
@@ -244,6 +246,8 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
||||||
#endif
|
#endif
|
||||||
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<ImagePacksModel>("org.kde.neochat", 1, 0, "ImagePacksModel");
|
||||||
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");
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ QQC2.Control {
|
|||||||
property bool isBusy: false
|
property bool isBusy: false
|
||||||
|
|
||||||
icon.name: "smiley"
|
icon.name: "smiley"
|
||||||
text: i18n("Add an Emoji")
|
text: i18n("Emojis & Stickers")
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
displayHint: Kirigami.DisplayHint.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ QQC2.Control {
|
|||||||
|
|
||||||
EmojiDialog {
|
EmojiDialog {
|
||||||
id: emojiDialog
|
id: emojiDialog
|
||||||
x: parent.width - implicitWidth
|
x: parent.width - width
|
||||||
y: -implicitHeight // - Kirigami.Units.smallSpacing
|
y: -implicitHeight // - Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
modal: false
|
modal: false
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ QQC2.ItemDelegate {
|
|||||||
property string name
|
property string name
|
||||||
property string emoji
|
property string emoji
|
||||||
property bool showTones: false
|
property bool showTones: false
|
||||||
|
property bool isImage: false
|
||||||
|
|
||||||
QQC2.ToolTip.text: emojiDelegate.name
|
QQC2.ToolTip.text: emojiDelegate.name
|
||||||
QQC2.ToolTip.visible: hovered && emojiDelegate.name !== ""
|
QQC2.ToolTip.visible: hovered && emojiDelegate.name !== ""
|
||||||
@@ -23,7 +24,7 @@ QQC2.ItemDelegate {
|
|||||||
contentItem: Item {
|
contentItem: Item {
|
||||||
Kirigami.Heading {
|
Kirigami.Heading {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !emojiDelegate.emoji.startsWith("image")
|
visible: !emojiDelegate.emoji.startsWith("image") && !emojiDelegate.isImage
|
||||||
text: emojiDelegate.emoji
|
text: emojiDelegate.emoji
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
@@ -40,7 +41,7 @@ QQC2.ItemDelegate {
|
|||||||
}
|
}
|
||||||
Image {
|
Image {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: emojiDelegate.emoji.startsWith("image")
|
visible: emojiDelegate.emoji.startsWith("image") || emojiDelegate.isImage
|
||||||
source: visible ? emojiDelegate.emoji : ""
|
source: visible ? emojiDelegate.emoji : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ QQC2.ScrollView {
|
|||||||
required property bool withCustom
|
required property bool withCustom
|
||||||
readonly property var searchCategory: withCustom ? EmojiModel.Search : EmojiModel.SearchNoCustom
|
readonly property var searchCategory: withCustom ? EmojiModel.Search : EmojiModel.SearchNoCustom
|
||||||
required property QtObject header
|
required property QtObject header
|
||||||
|
property bool stickers: false
|
||||||
|
|
||||||
signal chosen(string unicode)
|
signal chosen(string unicode)
|
||||||
|
signal stickerChosen(int index)
|
||||||
|
|
||||||
onActiveFocusChanged: if (activeFocus) {
|
onActiveFocusChanged: if (activeFocus) {
|
||||||
emojis.forceActiveFocus()
|
emojis.forceActiveFocus()
|
||||||
@@ -48,15 +50,19 @@ QQC2.ScrollView {
|
|||||||
delegate: EmojiDelegate {
|
delegate: EmojiDelegate {
|
||||||
id: emojiDelegate
|
id: emojiDelegate
|
||||||
checked: emojis.currentIndex === model.index
|
checked: emojis.currentIndex === model.index
|
||||||
emoji: modelData.unicode
|
emoji: !!modelData ? modelData.unicode : model.url
|
||||||
name: modelData.shortName
|
name: !!modelData ? modelData.shortName : model.body
|
||||||
|
|
||||||
width: emojis.cellWidth
|
width: emojis.cellWidth
|
||||||
height: emojis.cellHeight
|
height: emojis.cellHeight
|
||||||
|
|
||||||
|
isImage: emojiGrid.stickers
|
||||||
Keys.onEnterPressed: clicked()
|
Keys.onEnterPressed: clicked()
|
||||||
Keys.onReturnPressed: clicked()
|
Keys.onReturnPressed: clicked()
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
if (emojiGrid.stickers) {
|
||||||
|
emojiGrid.stickerChosen(model.index)
|
||||||
|
}
|
||||||
emojiGrid.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode)
|
emojiGrid.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode)
|
||||||
EmojiModel.emojiUsed(modelData)
|
EmojiModel.emojiUsed(modelData)
|
||||||
}
|
}
|
||||||
@@ -69,7 +75,7 @@ QQC2.ScrollView {
|
|||||||
tones.open()
|
tones.open()
|
||||||
tones.forceActiveFocus()
|
tones.forceActiveFocus()
|
||||||
}
|
}
|
||||||
showTones: EmojiModel.tones(modelData.shortName).length > 0
|
showTones: !!modelData && EmojiModel.tones(modelData.shortName).length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.PlaceholderMessage {
|
Kirigami.PlaceholderMessage {
|
||||||
|
|||||||
@@ -24,15 +24,45 @@ ColumnLayout {
|
|||||||
readonly property int categoryIconSize: Math.round(Kirigami.Units.gridUnit * 2.5)
|
readonly property int categoryIconSize: Math.round(Kirigami.Units.gridUnit * 2.5)
|
||||||
readonly property var currentCategory: currentEmojiModel[categories.currentIndex].category
|
readonly property var currentCategory: currentEmojiModel[categories.currentIndex].category
|
||||||
readonly property alias categoryCount: categories.count
|
readonly property alias categoryCount: categories.count
|
||||||
|
property int selectedType: 0
|
||||||
|
|
||||||
signal chosen(string emoji)
|
signal chosen(string emoji)
|
||||||
|
|
||||||
onActiveFocusChanged: if (activeFocus) {
|
onActiveFocusChanged: if (activeFocus) {
|
||||||
searchField.forceActiveFocus()
|
searchField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: root.categoryIconSize
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryIcon {
|
||||||
|
id: emojis
|
||||||
|
source: "smiley"
|
||||||
|
text: i18n("Emojis")
|
||||||
|
t: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryIcon {
|
||||||
|
id: stickers
|
||||||
|
source: "stickers"
|
||||||
|
text: i18n("Stickers")
|
||||||
|
t: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ScrollView {
|
QQC2.ScrollView {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
|
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
|
||||||
@@ -46,6 +76,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
Keys.onReturnPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
|
Keys.onReturnPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
|
||||||
Keys.onEnterPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
|
Keys.onEnterPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
|
||||||
|
|
||||||
KeyNavigation.down: emojiGrid.count > 0 ? emojiGrid : categories
|
KeyNavigation.down: emojiGrid.count > 0 ? emojiGrid : categories
|
||||||
KeyNavigation.tab: emojiGrid.count > 0 ? emojiGrid : categories
|
KeyNavigation.tab: emojiGrid.count > 0 ? emojiGrid : categories
|
||||||
|
|
||||||
@@ -54,22 +85,10 @@ ColumnLayout {
|
|||||||
Keys.forwardTo: searchField
|
Keys.forwardTo: searchField
|
||||||
interactive: width !== contentWidth
|
interactive: width !== contentWidth
|
||||||
|
|
||||||
model: root.currentEmojiModel
|
model: root.selectedType === 0 ? root.currentEmojiModel : stickerPackModel
|
||||||
Component.onCompleted: categories.forceActiveFocus()
|
Component.onCompleted: categories.forceActiveFocus()
|
||||||
|
|
||||||
delegate: EmojiDelegate {
|
delegate: root.selectedType === 0 ? emojiDelegate : stickerDelegate
|
||||||
width: root.categoryIconSize
|
|
||||||
height: width
|
|
||||||
|
|
||||||
checked: categories.currentIndex === model.index
|
|
||||||
emoji: modelData.emoji
|
|
||||||
name: modelData.name
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
categories.currentIndex = index
|
|
||||||
categories.focus = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +101,7 @@ ColumnLayout {
|
|||||||
id: searchField
|
id: searchField
|
||||||
Layout.margins: Kirigami.Units.smallSpacing
|
Layout.margins: Kirigami.Units.smallSpacing
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
visible: selectedType === 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The focus is manged by the parent and we don't want to use the standard
|
* The focus is manged by the parent and we don't want to use the standard
|
||||||
@@ -93,13 +113,15 @@ ColumnLayout {
|
|||||||
EmojiGrid {
|
EmojiGrid {
|
||||||
id: emojiGrid
|
id: emojiGrid
|
||||||
targetIconSize: root.currentCategory === EmojiModel.Custom ? Kirigami.Units.gridUnit * 3 : root.categoryIconSize // Custom emojis are bigger
|
targetIconSize: root.currentCategory === EmojiModel.Custom ? Kirigami.Units.gridUnit * 3 : root.categoryIconSize // Custom emojis are bigger
|
||||||
model: searchField.text.length === 0 ? EmojiModel.emojis(root.currentCategory) : (root.includeCustom ? EmojiModel.filterModel(searchField.text, false) : EmojiModel.filterModelNoCustom(searchField.text, false))
|
model: root.selectedType === 1 ? stickerModel : searchField.text.length === 0 ? EmojiModel.emojis(root.currentCategory) : (root.includeCustom ? EmojiModel.filterModel(searchField.text, false) : EmojiModel.filterModelNoCustom(searchField.text, false))
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
withCustom: root.includeCustom
|
withCustom: root.includeCustom
|
||||||
onChosen: root.chosen(unicode)
|
onChosen: root.chosen(unicode)
|
||||||
header: categories
|
header: categories
|
||||||
Keys.forwardTo: searchField
|
Keys.forwardTo: searchField
|
||||||
|
stickers: root.selectedType === 1
|
||||||
|
onStickerChosen: stickerModel.postSticker(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
@@ -132,4 +154,86 @@ ColumnLayout {
|
|||||||
orientation: Qt.Horizontal
|
orientation: Qt.Horizontal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImagePacksModel {
|
||||||
|
id: stickerPackModel
|
||||||
|
room: currentRoom
|
||||||
|
showStickers: true
|
||||||
|
showEmoticons: false
|
||||||
|
}
|
||||||
|
|
||||||
|
StickerModel {
|
||||||
|
id: stickerModel
|
||||||
|
model: stickerPackModel
|
||||||
|
packIndex: 0
|
||||||
|
room: currentRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: emojiDelegate
|
||||||
|
EmojiDelegate {
|
||||||
|
width: root.categoryIconSize
|
||||||
|
height: width
|
||||||
|
checked: categories.currentIndex === model.index
|
||||||
|
emoji: modelData ? modelData.emoji : ""
|
||||||
|
name: modelData ? modelData.name : ""
|
||||||
|
onClicked: {
|
||||||
|
categories.currentIndex = index;
|
||||||
|
categories.focus = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: stickerDelegate
|
||||||
|
EmojiDelegate {
|
||||||
|
width: root.categoryIconSize
|
||||||
|
height: width
|
||||||
|
emoji: model.avatarUrl ?? ""
|
||||||
|
isImage: true
|
||||||
|
name: model.displayName ?? ""
|
||||||
|
onClicked: stickerModel.packIndex = model.index
|
||||||
|
checked: stickerModel.packIndex === model.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component CategoryIcon : Kirigami.Icon {
|
||||||
|
id: categoryIcons
|
||||||
|
|
||||||
|
readonly property bool checked: root.selectedType === t
|
||||||
|
required property int t
|
||||||
|
required property string text
|
||||||
|
|
||||||
|
Layout.preferredWidth: root.categoryIconSize
|
||||||
|
Layout.preferredHeight: root.categoryIconSize
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: categoryIconsMouseArea.containsMouse
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: categoryIconsMouseArea
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.selectedType = t
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: categoryIcons.checked ? Kirigami.Theme.highlightColor : "transparent"
|
||||||
|
radius: Kirigami.Units.smallSpacing
|
||||||
|
z: -1
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
radius: Kirigami.Units.smallSpacing
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
opacity: categoryIconsMouseArea.containsMouse && !categoryIconsMouseArea.pressed ? 0.2 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ QQC2.Popup {
|
|||||||
padding: 2
|
padding: 2
|
||||||
|
|
||||||
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
|
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
|
||||||
width: Math.min(contentItem.categoryIconSize * contentItem.categoryCount + 2 * padding, QQC2.Overlay.overlay.width)
|
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, QQC2.Overlay.overlay.width)
|
||||||
contentItem: EmojiPicker {
|
contentItem: EmojiPicker {
|
||||||
id: emojiPicker
|
id: emojiPicker
|
||||||
height: 400
|
height: 400
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ Kirigami.CategorizedSettings {
|
|||||||
text: i18n("Notifications")
|
text: i18n("Notifications")
|
||||||
icon.name: "notifications"
|
icon.name: "notifications"
|
||||||
page: Qt.resolvedUrl("PushNotification.qml")
|
page: Qt.resolvedUrl("PushNotification.qml")
|
||||||
|
},
|
||||||
|
Kirigami.SettingAction {
|
||||||
|
text: i18n("Stickers")
|
||||||
|
icon.name: "stickers"
|
||||||
|
page: Qt.resolvedUrl("RoomStickers.qml")
|
||||||
initialProperties: {
|
initialProperties: {
|
||||||
return {
|
return {
|
||||||
room: root.room
|
room: root.room
|
||||||
|
|||||||
@@ -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("Emoticons.qml")
|
||||||
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
actionName: "spellChecking"
|
actionName: "spellChecking"
|
||||||
text: i18n("Spell Checking")
|
text: i18n("Spell Checking")
|
||||||
|
|||||||
109
src/stickermodel.cpp
Normal file
109
src/stickermodel.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "stickermodel.h"
|
||||||
|
|
||||||
|
#include "imagepackevent.h"
|
||||||
|
#include "imagepacksmodel.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
StickerModel::StickerModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
int StickerModel::rowCount(const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
return m_images.size();
|
||||||
|
}
|
||||||
|
QVariant StickerModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
const auto &row = index.row();
|
||||||
|
const auto &image = m_images[row];
|
||||||
|
if (role == Url) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
return m_room->connection()->makeMediaUrl(image.url);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (role == Body) {
|
||||||
|
if (image.body) {
|
||||||
|
return *image.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> StickerModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{StickerModel::Url, "url"},
|
||||||
|
{StickerModel::Body, "body"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ImagePacksModel *StickerModel::model() const
|
||||||
|
{
|
||||||
|
return m_model;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerModel::setModel(ImagePacksModel *model)
|
||||||
|
{
|
||||||
|
if (m_model) {
|
||||||
|
disconnect(m_model, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
connect(model, &ImagePacksModel::roomChanged, this, [this]() {
|
||||||
|
beginResetModel();
|
||||||
|
m_images = m_model->images(m_index);
|
||||||
|
endResetModel();
|
||||||
|
});
|
||||||
|
beginResetModel();
|
||||||
|
m_model = model;
|
||||||
|
m_images = m_model->images(m_index);
|
||||||
|
endResetModel();
|
||||||
|
Q_EMIT modelChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
int StickerModel::packIndex() const
|
||||||
|
{
|
||||||
|
return m_index;
|
||||||
|
}
|
||||||
|
void StickerModel::setPackIndex(int index)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
m_index = index;
|
||||||
|
if (m_model) {
|
||||||
|
m_images = m_model->images(m_index);
|
||||||
|
}
|
||||||
|
endResetModel();
|
||||||
|
Q_EMIT packIndexChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoChatRoom *StickerModel::room() const
|
||||||
|
{
|
||||||
|
return m_room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerModel::setRoom(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
m_room = room;
|
||||||
|
Q_EMIT roomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerModel::postSticker(int index)
|
||||||
|
{
|
||||||
|
const auto &image = m_images[index];
|
||||||
|
const auto &body = image.body ? *image.body : QString();
|
||||||
|
QJsonObject infoJson;
|
||||||
|
if (image.info) {
|
||||||
|
infoJson["w"] = image.info->imageSize.width();
|
||||||
|
infoJson["h"] = image.info->imageSize.height();
|
||||||
|
infoJson["mimetype"] = image.info->mimeType.name();
|
||||||
|
infoJson["size"] = image.info->payloadSize;
|
||||||
|
// TODO thumbnail
|
||||||
|
}
|
||||||
|
QJsonObject content{
|
||||||
|
{"body"_ls, body},
|
||||||
|
{"url"_ls, image.url.toString()},
|
||||||
|
{"info"_ls, infoJson},
|
||||||
|
};
|
||||||
|
m_room->postJson("m.sticker", content);
|
||||||
|
}
|
||||||
56
src/stickermodel.h
Normal file
56
src/stickermodel.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "imagepackevent.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class ImagePacksModel;
|
||||||
|
|
||||||
|
class StickerModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(ImagePacksModel *model READ model WRITE setModel NOTIFY modelChanged)
|
||||||
|
Q_PROPERTY(int packIndex READ packIndex WRITE setPackIndex NOTIFY packIndexChanged)
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
Url = Qt::UserRole + 1,
|
||||||
|
Body,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit StickerModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] ImagePacksModel *model() const;
|
||||||
|
void setModel(ImagePacksModel *model);
|
||||||
|
|
||||||
|
[[nodiscard]] int packIndex() const;
|
||||||
|
void setPackIndex(int index);
|
||||||
|
|
||||||
|
[[nodiscard]] NeoChatRoom *room() const;
|
||||||
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
Q_INVOKABLE void postSticker(int index);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void roomChanged();
|
||||||
|
void modelChanged();
|
||||||
|
void packIndexChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ImagePacksModel *m_model = nullptr;
|
||||||
|
int m_index = 0;
|
||||||
|
QVector<Quotient::ImagePackEventContent::ImagePackImage> m_images;
|
||||||
|
NeoChatRoom *m_room;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user