// SPDX-FileCopyrightText: Nheko Contributors // // SPDX-License-Identifier: GPL-3.0-or-later #include "SingleImagePackModel.h" #include #include #include #include #include #include #include #include "Cache_p.h" #include "ChatPage.h" #include "Logging.h" #include "MatrixClient.h" #include "Utils.h" #include "timeline/Permissions.h" #include "timeline/TimelineModel.h" SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) : QAbstractListModel(parent) , roomid_(std::move(pack_.source_room)) , statekey_(std::move(pack_.state_key)) , old_statekey_(statekey_) , pack(std::move(pack_.pack)) , fromSpace_(pack_.from_space) { if (!pack.pack) pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; shortcodes.reserve(pack.images.size()); for (const auto &e : pack.images) shortcodes.push_back(e.first); connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb); connect(this, &SingleImagePackModel::avatarUploaded, this, &SingleImagePackModel::setAvatarUrl); } int SingleImagePackModel::rowCount(const QModelIndex &) const { return (int)shortcodes.size(); } QHash SingleImagePackModel::roleNames() const { return { {Roles::Url, "url"}, {Roles::ShortCode, "shortCode"}, {Roles::Body, "body"}, {Roles::IsEmote, "isEmote"}, {Roles::IsSticker, "isSticker"}, }; } QVariant SingleImagePackModel::data(const QModelIndex &index, int role) const { if (hasIndex(index.row(), index.column(), index.parent())) { const auto &img = pack.images.at(shortcodes.at(index.row())); switch (role) { case Url: return QString::fromStdString(img.url); case ShortCode: return QString::fromStdString(shortcodes.at(index.row())); case Body: return QString::fromStdString(img.body); case IsEmote: return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); case IsSticker: return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); default: return {}; } } return {}; } bool SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role) { using mtx::events::msc2545::PackUsage; if (hasIndex(index.row(), index.column(), index.parent())) { auto &img = pack.images.at(shortcodes.at(index.row())); switch (role) { case ShortCode: { auto newCode = value.toString().toStdString(); // otherwise we delete this by accident newCode = unconflictingShortcode(newCode); auto tmp = img; auto oldCode = shortcodes.at(index.row()); pack.images.erase(oldCode); shortcodes[index.row()] = newCode; pack.images.insert({newCode, tmp}); emit dataChanged( this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); return true; } case Body: img.body = value.toString().toStdString(); emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::Body}); return true; case IsEmote: { bool isEmote = value.toBool(); bool isSticker = img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); img.usage.set(PackUsage::Emoji, isEmote); img.usage.set(PackUsage::Sticker, isSticker); if (img.usage == pack.pack->usage) img.usage.reset(); emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); return true; } case IsSticker: { bool isEmote = img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); bool isSticker = value.toBool(); img.usage.set(PackUsage::Emoji, isEmote); img.usage.set(PackUsage::Sticker, isSticker); if (img.usage == pack.pack->usage) img.usage.reset(); emit dataChanged( this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); return true; } } } return false; } bool SingleImagePackModel::isGloballyEnabled() const { if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { if (auto tmp = std::get_if>( &*roomPacks)) { if (tmp->content.rooms.count(roomid_) && tmp->content.rooms.at(roomid_).count(statekey_)) return true; } } return false; } void SingleImagePackModel::setGloballyEnabled(bool enabled) { mtx::events::msc2545::ImagePackRooms content{}; if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { if (auto tmp = std::get_if>( &*roomPacks)) { content = tmp->content; } } if (enabled) content.rooms[roomid_][statekey_] = {}; else content.rooms[roomid_].erase(statekey_); http::client()->put_account_data(content, [](mtx::http::RequestErr) { // emit this->globallyEnabledChanged(); }); } bool SingleImagePackModel::canEdit() const { if (roomid_.empty()) return true; else return Permissions(QString::fromStdString(roomid_)) .canChange(qml_mtx_events::ImagePackInRoom); } void SingleImagePackModel::setPackname(QString val) { auto val_ = val.toStdString(); if (val_ != this->pack.pack->display_name) { this->pack.pack->display_name = val_; emit packnameChanged(); } } void SingleImagePackModel::setAttribution(QString val) { auto val_ = val.toStdString(); if (val_ != this->pack.pack->attribution) { this->pack.pack->attribution = val_; emit attributionChanged(); } } void SingleImagePackModel::setAvatarUrl(QString val) { auto val_ = val.toStdString(); if (val_ != this->pack.pack->avatar_url) { this->pack.pack->avatar_url = val_; emit avatarUrlChanged(); } } QString SingleImagePackModel::avatarUrl() const { if (!pack.pack->avatar_url.empty()) return QString::fromStdString(pack.pack->avatar_url); else if (!pack.images.empty()) return QString::fromStdString(pack.images.begin()->second.url); else return QString(); } void SingleImagePackModel::setStatekey(QString val) { auto val_ = val.toStdString(); if (val_ != statekey_) { statekey_ = val_; // prevent deleting current pack if (!roomid_.empty() && statekey_ != old_statekey_) { statekey_ = unconflictingStatekey(roomid_, statekey_); } emit statekeyChanged(); } } void SingleImagePackModel::setIsStickerPack(bool val) { using mtx::events::msc2545::PackUsage; if (val != pack.pack->is_sticker()) { pack.pack->usage.set(PackUsage::Sticker, val); if (!val) pack.pack->usage.set(PackUsage::Emoji, true); emit isEmotePackChanged(); emit isStickerPackChanged(); } } void SingleImagePackModel::setIsEmotePack(bool val) { using mtx::events::msc2545::PackUsage; if (val != pack.pack->is_emoji()) { pack.pack->usage.set(PackUsage::Emoji, val); if (!val) pack.pack->usage.set(PackUsage::Sticker, true); emit isEmotePackChanged(); emit isStickerPackChanged(); } } void SingleImagePackModel::save() { if (roomid_.empty()) { http::client()->put_account_data(pack, [](mtx::http::RequestErr e) { if (e) ChatPage::instance()->showNotification( tr("Failed to update image pack: %1") .arg(QString::fromStdString(e->matrix_error.error))); }); } else { if (old_statekey_ != statekey_) { this->remove(); } http::client()->send_state_event( roomid_, statekey_, pack, [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { if (e) ChatPage::instance()->showNotification( tr("Failed to update image pack: %1") .arg(QString::fromStdString(e->matrix_error.error))); nhlog::net()->info("Uploaded image pack: %1", statekey_); }); } } void SingleImagePackModel::remove() { // handle account pack deletion. // Sadly we cannot actually delete the pack, // so we just send an empty pack to clear out its information. if (roomid_.empty()) { http::client()->put_account_data( mtx::events::msc2545::ImagePack(), [](mtx::http::RequestErr e) { if (e) ChatPage::instance()->showNotification( tr("Failed to update image pack: %1") .arg(QString::fromStdString(e->matrix_error.error))); }); return; } http::client()->send_state_event( roomid_, to_string(mtx::events::EventType::ImagePackInRoom), old_statekey_, nlohmann::json::object(), [](const mtx::responses::EventId &, mtx::http::RequestErr e) { if (e) ChatPage::instance()->showNotification( tr("Failed to delete old image pack: %1") .arg(QString::fromStdString(e->matrix_error.error))); }); old_statekey_ = statekey_; } void SingleImagePackModel::addStickers(QList files) { for (const auto &f : files) { auto file = QFile(f.toLocalFile()); if (!file.open(QFile::ReadOnly)) { ChatPage::instance()->showNotification( tr("Failed to open image: %1").arg(f.toLocalFile())); return; } auto bytes = file.readAll(); auto img = utils::readImage(bytes); mtx::common::ImageInfo info{}; auto sz = img.size() / 2; if (sz.width() > 512 || sz.height() > 512) { sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio); } else if (img.height() < 128 && img.width() < 128) { sz = img.size(); } info.h = sz.height(); info.w = sz.width(); info.size = bytes.size(); info.mimetype = QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(); auto filename = f.fileName().toStdString(); auto basename = QFileInfo(file).baseName().toStdString(); http::client()->upload( bytes.toStdString(), QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), filename, [this, basename, info](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) { if (e) { ChatPage::instance()->showNotification( tr("Failed to upload image: %1") .arg(QString::fromStdString(e->matrix_error.error))); return; } emit addImage(uri.content_uri, basename, info); }); } } void SingleImagePackModel::setAvatar(QUrl f) { auto file = QFile(f.toLocalFile()); if (!file.open(QFile::ReadOnly)) { ChatPage::instance()->showNotification(tr("Failed to open image: %1").arg(f.toLocalFile())); return; } auto bytes = file.readAll(); auto filename = f.fileName().toStdString(); http::client()->upload( bytes.toStdString(), QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), filename, [this, filename](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) { if (e) { ChatPage::instance()->showNotification( tr("Failed to upload image: %1") .arg(QString::fromStdString(e->matrix_error.error))); return; } emit avatarUploaded(QString::fromStdString(uri.content_uri)); }); } void SingleImagePackModel::remove(int idx) { if (idx < (int)shortcodes.size() && idx >= 0) { beginRemoveRows(QModelIndex(), idx, idx); auto s = shortcodes.at(idx); shortcodes.erase(shortcodes.begin() + idx); pack.images.erase(s); endRemoveRows(); } } std::string SingleImagePackModel::unconflictingShortcode(const std::string &shortcode) { if (pack.images.count(shortcode)) { // more images won't fit in an event anyway for (int i = 0; i < 64'000; i++) { auto tempCode = shortcode + std::to_string(i); if (!pack.images.count(tempCode)) { return tempCode; } } } return shortcode; } std::string SingleImagePackModel::unconflictingStatekey(const std::string &roomid, const std::string &key) { if (roomid.empty()) return key; std::unordered_set statekeys; auto currentPacks = cache::client()->getStateEventsWithType(roomid); for (const auto &pack : currentPacks) { if (!pack.content.images.empty()) statekeys.insert(pack.state_key); } auto defaultPack = cache::client()->getStateEvent(roomid); if (defaultPack && defaultPack->content.images.size()) { statekeys.insert(defaultPack->state_key); } if (statekeys.count(key)) { // arbitrary count. More than 64k image packs in a room are unlikely and if you have that, // you probably know what you are doing :) for (int i = 0; i < 64'000; i++) { auto tempCode = key + std::to_string(i); if (!statekeys.count(tempCode)) { return tempCode; } } } return key; } void SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info) { mtx::events::msc2545::PackImage img{}; img.url = uri; img.info = info; beginInsertRows( QModelIndex(), static_cast(shortcodes.size()), static_cast(shortcodes.size())); auto shortcode = unconflictingShortcode(filename); pack.images[shortcode] = img; shortcodes.push_back(shortcode); endInsertRows(); if (this->pack.pack->avatar_url.empty()) this->setAvatarUrl(QString::fromStdString(uri)); } #include "moc_SingleImagePackModel.cpp"