diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3ebed42d8..5f6c5db3c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/imagepackevent.cpp b/src/imagepackevent.cpp new file mode 100644 index 000000000..6539983f9 --- /dev/null +++ b/src/imagepackevent.cpp @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2021-2023 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "imagepackevent.h" +#include + +using namespace Quotient; + +ImagePackEventContent::ImagePackEventContent(const QJsonObject &json) +{ + if(json.contains(QStringLiteral("pack"))) { + pack = ImagePackEventContent::Pack{ + fromJson>(json["pack"].toObject()["display_name"]), +#ifdef QUOTIENT_07 + fromJson>(json["pack"].toObject()["avatar_url"]), +#else + QUrl(), +#endif + fromJson>(json["pack"].toObject()["usage"]), + fromJson>(json["pack"].toObject()["attribution"]), + }; + } else { + pack = none; + } + + const auto &keys = json["images"].toObject().keys(); + for (const auto &k : keys) { + Omittable 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(json["images"][k]["url"].toString()), +#else + QUrl(), +#endif + fromJson>(json["images"][k]["body"]), + info, + fromJson>(json["images"][k]["usage"]), + }; + } +} + +void ImagePackEventContent::fillJson(QJsonObject* o) const { + // TODO +} diff --git a/src/imagepackevent.h b/src/imagepackevent.h new file mode 100644 index 000000000..c737d18e1 --- /dev/null +++ b/src/imagepackevent.h @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2021-2023 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace Quotient +{ +class ImagePackEventContent +{ +public: + struct Pack { + Quotient::Omittable displayName; + Quotient::Omittable avatarUrl; + Quotient::Omittable usage; + Quotient::Omittable attribution; + }; + + struct ImagePackImage { + QString shortcode; + QUrl url; + Quotient::Omittable body; + Quotient::Omittable info; + Quotient::Omittable usage; + }; + + Quotient::Omittable pack; + QVector images; + + explicit ImagePackEventContent(const QJsonObject &o); + + void fillJson(QJsonObject *o) const; +}; + +#ifdef QUOTIENT_07 +class ImagePackEvent : public KeyedStateEventBase +#else +class ImagePackEvent : public StateEvent +#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) +} diff --git a/src/imagepacksmodel.cpp b/src/imagepacksmodel.cpp new file mode 100644 index 000000000..d2b9391cb --- /dev/null +++ b/src/imagepacksmodel.cpp @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "imagepacksmodel.h" +#include "imagepackevent.h" +#include "neochatroom.h" + +#include + +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 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(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(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 ImagePacksModel::images(int index) +{ + if (index < 0 || index >= m_events.size()) { + return {}; + } + return m_events[index].images; +} diff --git a/src/imagepacksmodel.h b/src/imagepacksmodel.h new file mode 100644 index 000000000..d02866f08 --- /dev/null +++ b/src/imagepacksmodel.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2021-2023 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include "imagepackevent.h" +#include +#include +#include + +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 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 images(int index); + +Q_SIGNALS: + void roomChanged(); + void showStickersChanged(); + void showEmoticonsChanged(); + +private: + QPointer m_room; + QVector m_events; + bool m_showStickers = true; + bool m_showEmoticons = true; +}; diff --git a/src/main.cpp b/src/main.cpp index 2ecda4084..92b382ee0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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("org.kde.neochat", 1, 0, "PollHandler"); #endif qmlRegisterType("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "StickerModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "ImagePacksModel"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM"); diff --git a/src/qml/Component/ChatBox/ChatBar.qml b/src/qml/Component/ChatBox/ChatBar.qml index 8ecffe7a8..a70c88b7c 100644 --- a/src/qml/Component/ChatBox/ChatBar.qml +++ b/src/qml/Component/ChatBox/ChatBar.qml @@ -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 diff --git a/src/qml/Component/Emoji/EmojiDelegate.qml b/src/qml/Component/Emoji/EmojiDelegate.qml index 9c0d712cd..4f128f49e 100644 --- a/src/qml/Component/Emoji/EmojiDelegate.qml +++ b/src/qml/Component/Emoji/EmojiDelegate.qml @@ -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 : "" } } diff --git a/src/qml/Component/Emoji/EmojiGrid.qml b/src/qml/Component/Emoji/EmojiGrid.qml index 9d3da1be3..6904ebcae 100644 --- a/src/qml/Component/Emoji/EmojiGrid.qml +++ b/src/qml/Component/Emoji/EmojiGrid.qml @@ -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 { diff --git a/src/qml/Component/Emoji/EmojiPicker.qml b/src/qml/Component/Emoji/EmojiPicker.qml index 8cfe3faa4..3b4024848 100644 --- a/src/qml/Component/Emoji/EmojiPicker.qml +++ b/src/qml/Component/Emoji/EmojiPicker.qml @@ -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 + } + } + } } diff --git a/src/qml/Dialog/EmojiDialog.qml b/src/qml/Dialog/EmojiDialog.qml index add5696d3..593d79634 100644 --- a/src/qml/Dialog/EmojiDialog.qml +++ b/src/qml/Dialog/EmojiDialog.qml @@ -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 diff --git a/src/qml/RoomSettings/Categories.qml b/src/qml/RoomSettings/Categories.qml index 82066c93f..5715eec48 100644 --- a/src/qml/RoomSettings/Categories.qml +++ b/src/qml/RoomSettings/Categories.qml @@ -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 diff --git a/src/qml/Settings/SettingsPage.qml b/src/qml/Settings/SettingsPage.qml index a1be11f83..e299cbe64 100644 --- a/src/qml/Settings/SettingsPage.qml +++ b/src/qml/Settings/SettingsPage.qml @@ -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") diff --git a/src/stickermodel.cpp b/src/stickermodel.cpp new file mode 100644 index 000000000..021d8be1e --- /dev/null +++ b/src/stickermodel.cpp @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2021-2023 Tobias Fella +// 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 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); +} diff --git a/src/stickermodel.h b/src/stickermodel.h new file mode 100644 index 000000000..b74c311d8 --- /dev/null +++ b/src/stickermodel.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include "imagepackevent.h" +#include "neochatroom.h" +#include +#include +#include + +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 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 m_images; + NeoChatRoom *m_room; +};