Implement sending stickers

MSC2545 image packs are used as source.
This commit is contained in:
Tobias Fella
2023-05-05 14:29:18 +00:00
parent 443d709eb8
commit 96c1b98d02
15 changed files with 622 additions and 24 deletions

View File

@@ -49,6 +49,9 @@ add_library(neochat STATIC
models/searchmodel.cpp
texthandler.cpp
logger.cpp
imagepackevent.cpp
stickermodel.cpp
imagepacksmodel.cpp
)
add_executable(neochat-app

54
src/imagepackevent.cpp Normal file
View 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
View 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
View 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
View 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;
};

View File

@@ -44,6 +44,7 @@
#include "clipboard.h"
#include "controller.h"
#include "filetypesingleton.h"
#include "imagepacksmodel.h"
#include "linkpreviewer.h"
#include "logger.h"
#include "login.h"
@@ -75,6 +76,7 @@
#include "models/statefiltermodel.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include "stickermodel.h"
#include "urlhelper.h"
#include "windowcontroller.h"
#ifdef QUOTIENT_07
@@ -244,6 +246,8 @@ int main(int argc, char *argv[])
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
#endif
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<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM");

View File

@@ -54,7 +54,7 @@ QQC2.Control {
property bool isBusy: false
icon.name: "smiley"
text: i18n("Add an Emoji")
text: i18n("Emojis & Stickers")
displayHint: Kirigami.DisplayHint.IconOnly
checkable: true
@@ -367,7 +367,7 @@ QQC2.Control {
EmojiDialog {
id: emojiDialog
x: parent.width - implicitWidth
x: parent.width - width
y: -implicitHeight // - Kirigami.Units.smallSpacing
modal: false

View File

@@ -11,6 +11,7 @@ QQC2.ItemDelegate {
property string name
property string emoji
property bool showTones: false
property bool isImage: false
QQC2.ToolTip.text: emojiDelegate.name
QQC2.ToolTip.visible: hovered && emojiDelegate.name !== ""
@@ -23,7 +24,7 @@ QQC2.ItemDelegate {
contentItem: Item {
Kirigami.Heading {
anchors.fill: parent
visible: !emojiDelegate.emoji.startsWith("image")
visible: !emojiDelegate.emoji.startsWith("image") && !emojiDelegate.isImage
text: emojiDelegate.emoji
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -40,7 +41,7 @@ QQC2.ItemDelegate {
}
Image {
anchors.fill: parent
visible: emojiDelegate.emoji.startsWith("image")
visible: emojiDelegate.emoji.startsWith("image") || emojiDelegate.isImage
source: visible ? emojiDelegate.emoji : ""
}
}

View File

@@ -16,8 +16,10 @@ QQC2.ScrollView {
required property bool withCustom
readonly property var searchCategory: withCustom ? EmojiModel.Search : EmojiModel.SearchNoCustom
required property QtObject header
property bool stickers: false
signal chosen(string unicode)
signal stickerChosen(int index)
onActiveFocusChanged: if (activeFocus) {
emojis.forceActiveFocus()
@@ -48,15 +50,19 @@ QQC2.ScrollView {
delegate: EmojiDelegate {
id: emojiDelegate
checked: emojis.currentIndex === model.index
emoji: modelData.unicode
name: modelData.shortName
emoji: !!modelData ? modelData.unicode : model.url
name: !!modelData ? modelData.shortName : model.body
width: emojis.cellWidth
height: emojis.cellHeight
isImage: emojiGrid.stickers
Keys.onEnterPressed: clicked()
Keys.onReturnPressed: clicked()
onClicked: {
if (emojiGrid.stickers) {
emojiGrid.stickerChosen(model.index)
}
emojiGrid.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode)
EmojiModel.emojiUsed(modelData)
}
@@ -69,7 +75,7 @@ QQC2.ScrollView {
tones.open()
tones.forceActiveFocus()
}
showTones: EmojiModel.tones(modelData.shortName).length > 0
showTones: !!modelData && EmojiModel.tones(modelData.shortName).length > 0
}
Kirigami.PlaceholderMessage {

View File

@@ -24,15 +24,45 @@ ColumnLayout {
readonly property int categoryIconSize: Math.round(Kirigami.Units.gridUnit * 2.5)
readonly property var currentCategory: currentEmojiModel[categories.currentIndex].category
readonly property alias categoryCount: categories.count
property int selectedType: 0
signal chosen(string emoji)
onActiveFocusChanged: if (activeFocus) {
searchField.forceActiveFocus()
searchField.forceActiveFocus();
}
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 {
Layout.fillWidth: true
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
@@ -46,6 +76,7 @@ ColumnLayout {
Keys.onReturnPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
Keys.onEnterPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
KeyNavigation.down: emojiGrid.count > 0 ? emojiGrid : categories
KeyNavigation.tab: emojiGrid.count > 0 ? emojiGrid : categories
@@ -54,22 +85,10 @@ ColumnLayout {
Keys.forwardTo: searchField
interactive: width !== contentWidth
model: root.currentEmojiModel
model: root.selectedType === 0 ? root.currentEmojiModel : stickerPackModel
Component.onCompleted: categories.forceActiveFocus()
delegate: EmojiDelegate {
width: root.categoryIconSize
height: width
checked: categories.currentIndex === model.index
emoji: modelData.emoji
name: modelData.name
onClicked: {
categories.currentIndex = index
categories.focus = true
}
}
delegate: root.selectedType === 0 ? emojiDelegate : stickerDelegate
}
}
@@ -82,6 +101,7 @@ ColumnLayout {
id: searchField
Layout.margins: Kirigami.Units.smallSpacing
Layout.fillWidth: true
visible: selectedType === 0
/**
* The focus is manged by the parent and we don't want to use the standard
@@ -93,13 +113,15 @@ ColumnLayout {
EmojiGrid {
id: emojiGrid
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.fillHeight: true
withCustom: root.includeCustom
onChosen: root.chosen(unicode)
header: categories
Keys.forwardTo: searchField
stickers: root.selectedType === 1
onStickerChosen: stickerModel.postSticker(index)
}
Kirigami.Separator {
@@ -132,4 +154,86 @@ ColumnLayout {
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
}
}
}
}

View File

@@ -48,7 +48,7 @@ QQC2.Popup {
padding: 2
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 {
id: emojiPicker
height: 400

View File

@@ -49,6 +49,11 @@ Kirigami.CategorizedSettings {
text: i18n("Notifications")
icon.name: "notifications"
page: Qt.resolvedUrl("PushNotification.qml")
},
Kirigami.SettingAction {
text: i18n("Stickers")
icon.name: "stickers"
page: Qt.resolvedUrl("RoomStickers.qml")
initialProperties: {
return {
room: root.room

View File

@@ -38,6 +38,12 @@ Kirigami.CategorizedSettings {
icon.name: "preferences-desktop-emoticons"
page: Qt.resolvedUrl("Emoticons.qml")
},
Kirigami.SettingAction {
actionName: "stickers"
text: i18n("Stickers")
icon.name: "stickers"
page: Qt.resolvedUrl("Emoticons.qml")
},
Kirigami.SettingAction {
actionName: "spellChecking"
text: i18n("Spell Checking")

109
src/stickermodel.cpp Normal file
View 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
View 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;
};