diff --git a/src/Cache.cpp b/src/Cache.cpp
index 7b6a6135..0bcf9fbf 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -3382,6 +3382,75 @@ Cache::getChildRoomIds(const std::string &room_id)
return roomids;
}
+std::vector<ImagePackInfo>
+Cache::getImagePacks(const std::string &room_id, bool stickers)
+{
+ auto txn = ro_txn(env_);
+ std::vector<ImagePackInfo> infos;
+
+ auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack) {
+ if (!pack.pack || (stickers ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
+ ImagePackInfo info;
+ if (pack.pack)
+ info.packname = pack.pack->display_name;
+
+ for (const auto &img : pack.images) {
+ if (img.second.overrides_usage() &&
+ (stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
+ continue;
+
+ info.images.insert(img);
+ }
+
+ if (!info.images.empty())
+ infos.push_back(std::move(info));
+ }
+ };
+
+ // packs from account data
+ if (auto accountpack =
+ getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) {
+ auto tmp =
+ std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(
+ &*accountpack);
+ if (tmp)
+ addPack(tmp->content);
+ }
+
+ // packs from rooms, that were enabled globally
+ if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) {
+ auto tmp =
+ std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
+ &*roomPacks);
+ if (tmp) {
+ for (const auto &[room_id2, state_to_d] : tmp->content.rooms) {
+ // don't add stickers from this room twice
+ if (room_id2 == room_id)
+ continue;
+
+ for (const auto &[state_id, d] : state_to_d) {
+ (void)d;
+ if (auto pack =
+ getStateEvent<mtx::events::msc2545::ImagePack>(
+ txn, room_id2, state_id))
+ addPack(pack->content);
+ }
+ }
+ }
+ }
+
+ // packs from current room
+ if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) {
+ addPack(pack->content);
+ }
+ for (const auto &pack :
+ getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) {
+ addPack(pack.content);
+ }
+
+ return infos;
+}
+
std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
{
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index 28c70055..f274d70f 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -11,6 +11,7 @@
#include <string>
#include <mtx/events/join_rules.hpp>
+#include <mtx/events/mscs/image_packs.hpp>
namespace cache {
enum class CacheVersion : int
@@ -109,3 +110,9 @@ struct RoomSearchResult
std::string room_id;
RoomInfo info;
};
+
+struct ImagePackInfo
+{
+ std::string packname;
+ std::map<std::string, mtx::events::msc2545::PackImage> images;
+};
diff --git a/src/Cache_p.h b/src/Cache_p.h
index d1f6307d..13fbc371 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -225,6 +225,8 @@ public:
std::vector<std::string> getParentRoomIds(const std::string &room_id);
std::vector<std::string> getChildRoomIds(const std::string &room_id);
+ std::vector<ImagePackInfo> getImagePacks(const std::string &room_id, bool stickers);
+
//! Mark a room that uses e2e encryption.
void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
bool isRoomEncrypted(const std::string &room_id);
diff --git a/src/ImagePackModel.cpp b/src/ImagePackModel.cpp
new file mode 100644
index 00000000..9b0dca8d
--- /dev/null
+++ b/src/ImagePackModel.cpp
@@ -0,0 +1,74 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "ImagePackModel.h"
+
+#include "Cache_p.h"
+#include "CompletionModelRoles.h"
+
+ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject *parent)
+ : QAbstractListModel(parent)
+ , room_id(roomId)
+{
+ auto packs = cache::client()->getImagePacks(room_id, stickers);
+
+ for (const auto &pack : packs) {
+ QString packname = QString::fromStdString(pack.packname);
+
+ for (const auto &img : pack.images) {
+ ImageDesc i{};
+ i.shortcode = QString::fromStdString(img.first);
+ i.packname = packname;
+ i.image = img.second;
+ images.push_back(std::move(i));
+ }
+ }
+}
+
+int
+ImagePackModel::rowCount(const QModelIndex &) const
+{
+ return (int)images.size();
+}
+
+QHash<int, QByteArray>
+ImagePackModel::roleNames() const
+{
+ return {
+ {CompletionModel::CompletionRole, "completionRole"},
+ {CompletionModel::SearchRole, "searchRole"},
+ {CompletionModel::SearchRole2, "searchRole2"},
+ {Roles::Url, "url"},
+ {Roles::ShortCode, "shortcode"},
+ {Roles::Body, "body"},
+ {Roles::PackName, "packname"},
+ {Roles::OriginalRow, "originalRow"},
+ };
+}
+
+QVariant
+ImagePackModel::data(const QModelIndex &index, int role) const
+{
+ if (hasIndex(index.row(), index.column(), index.parent())) {
+ switch (role) {
+ case CompletionModel::CompletionRole:
+ return QString::fromStdString(images[index.row()].image.url);
+ case Roles::Url:
+ return QString::fromStdString(images[index.row()].image.url);
+ case CompletionModel::SearchRole:
+ case Roles::ShortCode:
+ return images[index.row()].shortcode;
+ case CompletionModel::SearchRole2:
+ case Roles::Body:
+ return QString::fromStdString(images[index.row()].image.body);
+ case Roles::PackName:
+ return images[index.row()].packname;
+ case Roles::OriginalRow:
+ return index.row();
+ default:
+ return {};
+ }
+ }
+ return {};
+}
diff --git a/src/ImagePackModel.h b/src/ImagePackModel.h
new file mode 100644
index 00000000..937014ec
--- /dev/null
+++ b/src/ImagePackModel.h
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+
+#include <mtx/events/mscs/image_packs.hpp>
+
+class ImagePackModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Roles
+ {
+ Url = Qt::UserRole,
+ ShortCode,
+ Body,
+ PackName,
+ OriginalRow,
+ };
+
+ ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ mtx::events::msc2545::PackImage imageAt(int row)
+ {
+ if (row < 0 || static_cast<size_t>(row) >= images.size())
+ return {};
+ return images.at(static_cast<size_t>(row)).image;
+ }
+
+private:
+ std::string room_id;
+
+ struct ImageDesc
+ {
+ QString shortcode;
+ QString packname;
+
+ mtx::events::msc2545::PackImage image;
+ };
+
+ std::vector<ImageDesc> images;
+};
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index b0747a7c..0f210722 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -21,6 +21,7 @@
#include "ChatPage.h"
#include "CompletionProxyModel.h"
#include "Config.h"
+#include "ImagePackModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@@ -502,6 +503,22 @@ InputBar::video(const QString &filename,
}
void
+InputBar::sticker(ImagePackModel *model, int row)
+{
+ if (!model || row < 0)
+ return;
+
+ auto img = model->imageAt(row);
+
+ mtx::events::msg::StickerImage sticker{};
+ sticker.info = img.info.value_or(mtx::common::ImageInfo{});
+ sticker.url = img.url;
+ sticker.body = img.body;
+
+ room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
+}
+
+void
InputBar::command(QString command, QString args)
{
if (command == "me") {
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index c9728379..acedceb7 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -12,6 +12,7 @@
#include <mtx/responses/messages.hpp>
class TimelineModel;
+class ImagePackModel;
class QMimeData;
class QDropEvent;
class QStringList;
@@ -57,6 +58,7 @@ public slots:
MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
bool rainbowify = false);
void reaction(const QString &reactedEvent, const QString &reactionKey);
+ void sticker(ImagePackModel *model, int row);
private slots:
void startTyping();
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 5832f56e..abfe28a9 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -1300,6 +1300,14 @@ struct SendMessageVisitor
sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
mtx::events::EventType::KeyVerificationCancel>(msg);
}
+ void operator()(mtx::events::Sticker msg)
+ {
+ msg.type = mtx::events::EventType::Sticker;
+ if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
+ model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
+ } else
+ emit model_->addPendingMessageToStore(msg);
+ }
TimelineModel *model_;
};
@@ -1309,6 +1317,7 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
{
std::visit(
[](auto &msg) {
+ // gets overwritten for reactions and stickers in SendMessageVisitor
msg.type = mtx::events::EventType::RoomMessage;
msg.event_id = "m" + http::client()->generate_txn_id();
msg.sender = http::client()->user_id().to_string();
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index b67234f2..0e2895d4 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -410,10 +410,17 @@ template<class T>
void
TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)
{
- mtx::events::RoomEvent<T> msgCopy = {};
- msgCopy.content = content;
- msgCopy.type = eventType;
- emit newMessageToSend(msgCopy);
+ if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) {
+ mtx::events::Sticker msgCopy = {};
+ msgCopy.content = content;
+ msgCopy.type = eventType;
+ emit newMessageToSend(msgCopy);
+ } else {
+ mtx::events::RoomEvent<T> msgCopy = {};
+ msgCopy.content = content;
+ msgCopy.type = eventType;
+ emit newMessageToSend(msgCopy);
+ }
resetReply();
resetEdit();
}
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b39ef615..3e69f92b 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -19,6 +19,7 @@
#include "DelegateChooser.h"
#include "DeviceVerificationFlow.h"
#include "EventAccessors.h"
+#include "ImagePackModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@@ -144,6 +145,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
+ qRegisterMetaType<ImagePackModel *>();
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
@@ -593,6 +595,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
auto proxy = new CompletionProxyModel(roomModel);
roomModel->setParent(proxy);
return proxy;
+ } else if (completerName == "stickers") {
+ auto stickerModel = new ImagePackModel(roomId.toStdString(), true);
+ auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4);
+ stickerModel->setParent(proxy);
+ return proxy;
}
return nullptr;
}
|