diff --git a/CMakeLists.txt b/CMakeLists.txt index 62db167f..dad4fb16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -456,6 +456,8 @@ set(SRC_FILES src/ColorImageProvider.h src/CombinedImagePackModel.cpp src/CombinedImagePackModel.h + src/GridImagePackModel.cpp + src/GridImagePackModel.h src/CommandCompleter.cpp src/CommandCompleter.h src/CompletionModelRoles.h diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 6ddbb32e..14f27fff 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -433,7 +433,7 @@ Rectangle { ToolTip.visible: hovered ToolTip.text: qsTr("Stickers") onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) { - room.input.sticker(stickerPopup.model.sourceModel, row); + room.input.sticker(row); TimelineManager.focusMessageInput(); }) diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index 1928dfa7..f84fe06f 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -102,19 +102,41 @@ Menu { } } - // emoji grid - GridView { + Component { + id: sectionHeading + Rectangle { + width: gridView.width + height: childrenRect.height + color: Nheko.colors.alternateBase + + required property string section + + Text { + anchors.left: parent.left + anchors.right: parent.right + text: parent.section + color: Nheko.colors.text + font.bold: true + } + } + } + + // sticker grid + ListView { id: gridView - model: roomid ? TimelineManager.completerFor("stickers", roomid) : null + model: roomid ? TimelineManager.completerFor("stickergrid", roomid) : null Layout.preferredHeight: cellHeight * 3.5 Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 - Nheko.paddingSmall - cellWidth: stickerDimPad - cellHeight: stickerDimPad + property int cellHeight: stickerDimPad boundsBehavior: Flickable.StopAtBounds clip: true currentIndex: -1 // prevent sorting from stealing focus - cacheBuffer: 500 + + section.property: "packname" + section.criteria: ViewSection.FullString + section.delegate: sectionHeading + section.labelPositioning: ViewSection.InlineLabels | ViewSection.CurrentLabelAtStart ScrollHelper { flickable: parent @@ -123,23 +145,29 @@ Menu { } // Individual emoji - delegate: AbstractButton { + delegate: Row { + required property var row; + + Repeater { + model: row + + delegate: AbstractButton { width: stickerDim height: stickerDim hoverEnabled: true - ToolTip.text: ":" + model.shortcode + ": - " + model.body + ToolTip.text: ":" + modelData.shortcode + ": - " + modelData.body ToolTip.visible: hovered // TODO: maybe add favorites at some point? onClicked: { - console.debug("Picked " + model.shortcode); + console.debug("Picked " + modelData.descriptor); stickerPopup.close(); - callback(model.originalRow); + callback(modelData.descriptor); } contentItem: Image { height: stickerDim width: stickerDim - source: model.url.replace("mxc://", "image://MxcImage/") + "?scale" + source: modelData.url.replace("mxc://", "image://MxcImage/") + "?scale" fillMode: Image.PreserveAspectFit } @@ -150,6 +178,8 @@ Menu { } } + } + } ScrollBar.vertical: ScrollBar { id: emojiScroll diff --git a/src/GridImagePackModel.cpp b/src/GridImagePackModel.cpp new file mode 100644 index 00000000..4fee086a --- /dev/null +++ b/src/GridImagePackModel.cpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "GridImagePackModel.h" + +#include "Cache_p.h" +#include "CompletionModelRoles.h" + +#include + +Q_DECLARE_METATYPE(StickerImage) + +GridImagePackModel::GridImagePackModel(const std::string &roomId, bool stickers, QObject *parent) + : QAbstractListModel(parent) + , room_id(roomId) +{ + [[maybe_unused]] static auto id = qRegisterMetaType(); + + auto originalPacks = cache::client()->getImagePacks(room_id, stickers); + + for (auto &pack : originalPacks) { + PackDesc newPack{}; + newPack.packname = + pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : QString(); + newPack.room_id = pack.source_room; + newPack.state_key = pack.state_key; + + newPack.images.resize(pack.pack.images.size()); + std::ranges::transform(std::move(pack.pack.images), newPack.images.begin(), [](auto &&img) { + return std::pair(std::move(img.second), QString::fromStdString(img.first)); + }); + + size_t packRowCount = + (newPack.images.size() / columns) + (newPack.images.size() % columns ? 1 : 0); + newPack.firstRow = rowToPack.size(); + for (size_t i = 0; i < packRowCount; i++) + rowToPack.push_back(packs.size()); + packs.push_back(std::move(newPack)); + } +} + +int +GridImagePackModel::rowCount(const QModelIndex &) const +{ + return (int)rowToPack.size(); +} + +QHash +GridImagePackModel::roleNames() const +{ + return { + {Roles::PackName, "packname"}, + {Roles::Row, "row"}, + }; +} + +QVariant +GridImagePackModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < rowCount() && index.row() >= 0) { + const auto &pack = packs[rowToPack[index.row()]]; + switch (role) { + case Roles::PackName: + return pack.packname; + case Roles::Row: { + std::size_t offset = static_cast(index.row()) - pack.firstRow; + QList imgs; + auto endOffset = std::min((offset + 1) * 3, pack.images.size()); + for (std::size_t img = offset * 3; img < endOffset; img++) { + const auto &data = pack.images.at(img); + imgs.push_back({.url = QString::fromStdString(data.first.url), + .shortcode = data.second, + .body = QString::fromStdString(data.first.body), + .descriptor_ = std::vector{ + pack.room_id, + pack.state_key, + data.second.toStdString(), + }}); + } + return QVariant::fromValue(imgs); + } + default: + return {}; + } + } + return {}; +} diff --git a/src/GridImagePackModel.h b/src/GridImagePackModel.h new file mode 100644 index 00000000..1345b103 --- /dev/null +++ b/src/GridImagePackModel.h @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include + +struct StickerImage +{ + Q_GADGET + Q_PROPERTY(QString url MEMBER url CONSTANT) + Q_PROPERTY(QString shortcode MEMBER shortcode CONSTANT) + Q_PROPERTY(QString body MEMBER body CONSTANT) + Q_PROPERTY(QStringList descriptor READ descriptor CONSTANT) + +public: + QStringList descriptor() const + { + if (descriptor_.size() == 3) + return QStringList{ + QString::fromStdString(descriptor_[0]), + QString::fromStdString(descriptor_[1]), + QString::fromStdString(descriptor_[2]), + }; + else + return {}; + } + + QString url; + QString shortcode; + QString body; + + std::vector descriptor_; // roomid, statekey, shortcode +}; + +class GridImagePackModel final : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + PackName = Qt::UserRole, + Row, + }; + + GridImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + +private: + std::string room_id; + + struct PackDesc + { + QString packname; + QString packavatar; + std::string room_id, state_key; + + std::vector> images; + std::size_t firstRow; + }; + + std::vector packs; + std::vector rowToPack; + int columns = 3; +}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 3ba40bb6..d9a03346 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -19,6 +19,7 @@ #include "CompletionProxyModel.h" #include "Config.h" #include "EventAccessors.h" +#include "GridImagePackModel.h" #include "ImagePackListModel.h" #include "InviteesModel.h" #include "JdenticonProvider.h" @@ -150,6 +151,7 @@ MainWindow::registerQmlTypes() qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType>(); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index dd6813c2..0c2e3752 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -836,28 +836,40 @@ InputBar::getCommandAndArgs(const QString ¤tText) const } void -InputBar::sticker(CombinedImagePackModel *model, int row) +InputBar::sticker(QStringList descriptor) { - if (!model || row < 0) + if (descriptor.size() != 3) return; - auto img = model->imageAt(row); + auto originalPacks = cache::client()->getImagePacks(room->roomId().toStdString(), true); - mtx::events::msg::StickerImage sticker{}; - sticker.info = img.info.value_or(mtx::common::ImageInfo{}); - sticker.url = img.url; - sticker.body = img.body.empty() ? model->shortcodeAt(row).toStdString() : img.body; + auto source_room = descriptor[0].toStdString(); + auto state_key = descriptor[1].toStdString(); + auto short_code = descriptor[2].toStdString(); - // workaround for https://github.com/vector-im/element-ios/issues/2353 - sticker.info.thumbnail_url = sticker.url; - sticker.info.thumbnail_info.mimetype = sticker.info.mimetype; - sticker.info.thumbnail_info.size = sticker.info.size; - sticker.info.thumbnail_info.h = sticker.info.h; - sticker.info.thumbnail_info.w = sticker.info.w; + for (auto &pack : originalPacks) { + if (pack.source_room == source_room && pack.state_key == state_key && + pack.pack.images.contains(short_code)) { + auto img = pack.pack.images.at(short_code); - sticker.relations = generateRelations(); + mtx::events::msg::StickerImage sticker{}; + sticker.info = img.info.value_or(mtx::common::ImageInfo{}); + sticker.url = img.url; + sticker.body = img.body.empty() ? short_code : img.body; - room->sendMessageEvent(sticker, mtx::events::EventType::Sticker); + // workaround for https://github.com/vector-im/element-ios/issues/2353 + sticker.info.thumbnail_url = sticker.url; + sticker.info.thumbnail_info.mimetype = sticker.info.mimetype; + sticker.info.thumbnail_info.size = sticker.info.size; + sticker.info.thumbnail_info.h = sticker.info.h; + sticker.info.thumbnail_info.w = sticker.info.w; + + sticker.relations = generateRelations(); + + room->sendMessageEvent(sticker, mtx::events::EventType::Sticker); + break; + } + } } bool diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index b2db377f..1f1d6fe1 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -217,7 +217,7 @@ public slots: MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); void reaction(const QString &reactedEvent, const QString &reactionKey); - void sticker(CombinedImagePackModel *model, int row); + void sticker(QStringList descriptor); void acceptUploads(); void declineUploads(); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 44f288c6..4b171dc4 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -17,6 +17,7 @@ #include "CommandCompleter.h" #include "CompletionProxyModel.h" #include "EventAccessors.h" +#include "GridImagePackModel.h" #include "ImagePackListModel.h" #include "InviteesModel.h" #include "Logging.h" @@ -477,6 +478,9 @@ TimelineViewManager::completerFor(const QString &completerName, const QString &r auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast(-1) / 4); stickerModel->setParent(proxy); return proxy; + } else if (completerName == QLatin1String("stickergrid")) { + auto stickerModel = new GridImagePackModel(roomId.toStdString(), true); + return stickerModel; } else if (completerName == QLatin1String("customEmoji")) { auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), false); auto proxy = new CompletionProxyModel(stickerModel);