diff --git a/CMakeLists.txt b/CMakeLists.txt index 905e6159..e0d9b5a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -297,6 +297,9 @@ set(SRC_FILES src/ui/UserProfile.cpp src/ui/RoomSettings.cpp + # Generic notification stuff + src/notifications/Manager.cpp + src/AvatarProvider.cpp src/BlurhashProvider.cpp src/Cache.cpp @@ -557,7 +560,7 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC}) if (APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa") - set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm) + set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/notifications/ManagerMac.cpp src/emoji/MacHelper.mm) if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0") set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON) endif() diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index 1834e040..b9962cef 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -12,13 +13,14 @@ #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" +#include "MxcImageProvider.h" #include "Utils.h" static QPixmapCache avatar_cache; namespace AvatarProvider { void -resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback) +resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback) { const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); @@ -33,44 +35,32 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca return; } - auto data = cache::image(cacheKey); - if (!data.isNull()) { - pixmap = QPixmap::fromImage(utils::readImage(&data)); - avatar_cache.insert(cacheKey, pixmap); - callback(pixmap); - return; - } + MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")), + QSize(size, size), + [callback, cacheKey, recv = QPointer(receiver)]( + QString, QSize, QImage img, QString) { + if (!recv) + return; - auto proxy = std::make_shared(); - QObject::connect(proxy.get(), - &AvatarProxy::avatarDownloaded, - receiver, - [callback, cacheKey](QByteArray data) { - QPixmap pm = QPixmap::fromImage(utils::readImage(&data)); - avatar_cache.insert(cacheKey, pm); - callback(pm); - }); + auto proxy = std::make_shared(); + QObject::connect(proxy.get(), + &AvatarProxy::avatarDownloaded, + recv, + [callback, cacheKey](QPixmap pm) { + if (!pm.isNull()) + avatar_cache.insert( + cacheKey, pm); + callback(pm); + }); - mtx::http::ThumbOpts opts; - opts.width = size; - opts.height = size; - opts.mxc_url = avatarUrl.toStdString(); + if (img.isNull()) { + emit proxy->avatarDownloaded(QPixmap{}); + return; + } - http::client()->get_thumbnail( - opts, - [opts, cacheKey, proxy = std::move(proxy)](const std::string &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to download avatar: {} - ({} {})", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - } else { - cache::saveImage(cacheKey.toStdString(), res); - } - - emit proxy->avatarDownloaded(QByteArray(res.data(), (int)res.size())); - }); + auto pm = QPixmap::fromImage(std::move(img)); + emit proxy->avatarDownloaded(pm); + }); } void @@ -80,8 +70,8 @@ resolve(const QString &room_id, QObject *receiver, AvatarCallback callback) { - const auto avatarUrl = cache::avatarUrl(room_id, user_id); + auto avatarUrl = cache::avatarUrl(room_id, user_id); - resolve(avatarUrl, size, receiver, callback); + resolve(std::move(avatarUrl), size, receiver, callback); } } diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h index 0bea1a8f..173a2fba 100644 --- a/src/AvatarProvider.h +++ b/src/AvatarProvider.h @@ -8,19 +8,19 @@ #include #include +using AvatarCallback = std::function; + class AvatarProxy : public QObject { Q_OBJECT signals: - void avatarDownloaded(const QByteArray &data); + void avatarDownloaded(QPixmap pm); }; -using AvatarCallback = std::function; - namespace AvatarProvider { void -resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb); +resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback cb); void resolve(const QString &room_id, const QString &user_id, diff --git a/src/Cache.cpp b/src/Cache.cpp index ec0f2858..4423b21f 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -55,9 +55,6 @@ constexpr auto BATCH_SIZE = 100; //! Format: room_id -> RoomInfo constexpr auto ROOMS_DB("rooms"); constexpr auto INVITES_DB("invites"); -//! Keeps already downloaded media for reuse. -//! Format: matrix_url -> binary data. -constexpr auto MEDIA_DB("media"); //! Information that must be kept between sync requests. constexpr auto SYNC_STATE_DB("sync_state"); //! Read receipts per room/event. @@ -244,7 +241,6 @@ Cache::setup() syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); - mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE); readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); @@ -700,82 +696,6 @@ Cache::secret(const std::string &name) return secret.toStdString(); } -// -// Media Management -// - -void -Cache::saveImage(const std::string &url, const std::string &img_data) -{ - if (url.empty() || img_data.empty()) - return; - - try { - auto txn = lmdb::txn::begin(env_); - - mediaDb_.put(txn, url, img_data); - - txn.commit(); - } catch (const lmdb::error &e) { - nhlog::db()->critical("saveImage: {}", e.what()); - } -} - -void -Cache::saveImage(const QString &url, const QByteArray &image) -{ - saveImage(url.toStdString(), std::string(image.constData(), image.length())); -} - -QByteArray -Cache::image(lmdb::txn &txn, const std::string &url) -{ - if (url.empty()) - return QByteArray(); - - try { - std::string_view image; - bool res = mediaDb_.get(txn, url, image); - - if (!res) - return QByteArray(); - - return QByteArray(image.data(), (int)image.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("image: {}, {}", e.what(), url); - } - - return QByteArray(); -} - -QByteArray -Cache::image(const QString &url) -{ - if (url.isEmpty()) - return QByteArray(); - - auto key = url.toStdString(); - - try { - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - std::string_view image; - - bool res = mediaDb_.get(txn, key, image); - - txn.commit(); - - if (!res) - return QByteArray(); - - return QByteArray(image.data(), (int)image.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("image: {} {}", e.what(), url.toStdString()); - } - - return QByteArray(); -} - void Cache::removeInvite(lmdb::txn &txn, const std::string &room_id) { @@ -860,7 +780,6 @@ Cache::deleteData() lmdb::dbi_close(env_, syncStateDb_); lmdb::dbi_close(env_, roomsDb_); lmdb::dbi_close(env_, invitesDb_); - lmdb::dbi_close(env_, mediaDb_); lmdb::dbi_close(env_, readReceiptsDb_); lmdb::dbi_close(env_, notificationsDb_); @@ -2470,50 +2389,6 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) return QString(); } -QImage -Cache::getRoomAvatar(const QString &room_id) -{ - return getRoomAvatar(room_id.toStdString()); -} - -QImage -Cache::getRoomAvatar(const std::string &room_id) -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - std::string_view response; - - if (!roomsDb_.get(txn, room_id, response)) { - txn.commit(); - return QImage(); - } - - std::string media_url; - - try { - RoomInfo info = json::parse(response); - media_url = std::move(info.avatar_url); - - if (media_url.empty()) { - txn.commit(); - return QImage(); - } - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info: {}, {}", - e.what(), - std::string(response.data(), response.size())); - } - - if (!mediaDb_.get(txn, media_url, response)) { - txn.commit(); - return QImage(); - } - - txn.commit(); - - return QImage::fromData(QByteArray(response.data(), (int)response.size())); -} - std::vector Cache::joinedRooms() { @@ -2615,8 +2490,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_ MemberInfo tmp = json::parse(user_data); members.emplace_back( RoomMember{QString::fromStdString(std::string(user_id)), - QString::fromStdString(tmp.name), - QImage::fromData(image(txn, tmp.avatar_url))}); + QString::fromStdString(tmp.name)}); } catch (const json::exception &e) { nhlog::db()->warn("{}", e.what()); } @@ -4240,18 +4114,6 @@ hasEnoughPowerLevel(const std::vector &eventTypes, return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); } -//! Retrieves the saved room avatar. -QImage -getRoomAvatar(const QString &id) -{ - return instance_->getRoomAvatar(id); -} -QImage -getRoomAvatar(const std::string &id) -{ - return instance_->getRoomAvatar(id); -} - void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) { @@ -4276,27 +4138,6 @@ lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) return instance_->lastInvisibleEventAfter(room_id, event_id); } -QByteArray -image(const QString &url) -{ - return instance_->image(url); -} -QByteArray -image(lmdb::txn &txn, const std::string &url) -{ - return instance_->image(txn, url); -} -void -saveImage(const std::string &url, const std::string &data) -{ - instance_->saveImage(url, data); -} -void -saveImage(const QString &url, const QByteArray &data) -{ - instance_->saveImage(url, data); -} - RoomInfo singleRoomInfo(const std::string &room_id) { diff --git a/src/Cache.h b/src/Cache.h index f7e5f749..e795b32a 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -6,8 +6,6 @@ #pragma once #include -#include -#include #include #if __has_include() @@ -135,12 +133,6 @@ hasEnoughPowerLevel(const std::vector &eventTypes, const std::string &room_id, const std::string &user_id); -//! Retrieves the saved room avatar. -QImage -getRoomAvatar(const QString &id); -QImage -getRoomAvatar(const std::string &id); - //! Adds a user to the read list for the given event. //! //! There should be only one user id present in a receipt list per room. @@ -162,20 +154,6 @@ getEventIndex(const std::string &room_id, std::string_view event_id); std::optional> lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); -QByteArray -image(const QString &url); -QByteArray -image(lmdb::txn &txn, const std::string &url); -inline QByteArray -image(const std::string &url) -{ - return image(QString::fromStdString(url)); -} -void -saveImage(const std::string &url, const std::string &data); -void -saveImage(const QString &url, const QByteArray &data); - RoomInfo singleRoomInfo(const std::string &room_id); std::map diff --git a/src/CacheStructs.h b/src/CacheStructs.h index ad9aab98..c449f013 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -25,7 +25,6 @@ struct RoomMember { QString user_id; QString display_name; - QImage avatar; }; //! Used to uniquely identify a list of read receipts. diff --git a/src/Cache_p.h b/src/Cache_p.h index 3454cd54..62927923 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -118,10 +118,6 @@ public: const std::string &room_id, const std::string &user_id); - //! Retrieves the saved room avatar. - QImage getRoomAvatar(const QString &id); - QImage getRoomAvatar(const std::string &id); - //! Adds a user to the read list for the given event. //! //! There should be only one user id present in a receipt list per room. @@ -137,11 +133,6 @@ public: using UserReceipts = std::multimap>; UserReceipts readReceipts(const QString &event_id, const QString &room_id); - QByteArray image(const QString &url); - QByteArray image(lmdb::txn &txn, const std::string &url); - void saveImage(const std::string &url, const std::string &data); - void saveImage(const QString &url, const QByteArray &data); - RoomInfo singleRoomInfo(const std::string &room_id); std::vector roomsWithStateUpdates(const mtx::responses::Sync &res); std::vector roomsWithTagUpdates(const mtx::responses::Sync &res); @@ -528,7 +519,6 @@ private: lmdb::dbi syncStateDb_; lmdb::dbi roomsDb_; lmdb::dbi invitesDb_; - lmdb::dbi mediaDb_; lmdb::dbi readReceiptsDb_; lmdb::dbi notificationsDb_; diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp index f644ebee..7cc5d10e 100644 --- a/src/CommunitiesList.cpp +++ b/src/CommunitiesList.cpp @@ -6,6 +6,7 @@ #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" +#include "MxcImageProvider.h" #include "Splitter.h" #include "UserSettingsPage.h" @@ -253,37 +254,16 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id) void CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) { - auto savedImgData = cache::image(avatarUrl); - if (!savedImgData.isNull()) { - QPixmap pix; - pix.loadFromData(savedImgData); - emit avatarRetrieved(id, pix); - return; - } - - if (avatarUrl.isEmpty()) - return; - - mtx::http::ThumbOpts opts; - opts.mxc_url = avatarUrl.toStdString(); - http::client()->get_thumbnail( - opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to download avatar: {} - ({} {})", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); + MxcImageProvider::download( + QString(avatarUrl).remove(QStringLiteral("mxc://")), + QSize(96, 96), + [this, id](QString, QSize, QImage img, QString) { + if (img.isNull()) { + nhlog::net()->warn("failed to download avatar: {})", id.toStdString()); return; } - cache::saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), (int)res.size()); - - QPixmap pix; - pix.loadFromData(data); - - emit avatarRetrieved(id, pix); + emit avatarRetrieved(id, QPixmap::fromImage(img)); }); } diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index e4f629a5..a20657c8 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -4,106 +4,201 @@ #include "MxcImageProvider.h" +#include + #include -#include "Cache.h" +#include +#include +#include +#include + #include "Logging.h" #include "MatrixClient.h" #include "Utils.h" +QHash infos; + +QQuickImageResponse * +MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) +{ + MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + pool.start(response); + return response; +} + +void +MxcImageProvider::addEncryptionInfo(mtx::crypto::EncryptedFile info) +{ + infos.insert(QString::fromStdString(info.url), info); +} void MxcImageResponse::run() { - if (m_requestedSize.isValid() && !m_encryptionInfo) { - QString fileName = QString("%1_%2x%3_crop") - .arg(m_id) - .arg(m_requestedSize.width()) - .arg(m_requestedSize.height()); + MxcImageProvider::download( + m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) { + if (image.isNull()) { + m_error = "Failed to download image."; + } else { + m_image = image; + } + emit finished(); + }); +} - auto data = cache::image(fileName); - if (!data.isNull()) { - m_image = utils::readImage(&data); +void +MxcImageProvider::download(const QString &id, + const QSize &requestedSize, + std::function then) +{ + std::optional encryptionInfo; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + encryptionInfo = *temp; - if (!m_image.isNull()) { - m_image = m_image.scaled( - m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - m_image.setText("mxc url", "mxc://" + m_id); + if (requestedSize.isValid() && !encryptionInfo) { + QString fileName = + QString("%1_%2x%3_crop") + .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding | + QByteArray::OmitTrailingEquals))) + .arg(requestedSize.width()) + .arg(requestedSize.height()); + QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/media_cache", + fileName); + QDir().mkpath(fileInfo.absolutePath()); - if (!m_image.isNull()) { - emit finished(); + if (fileInfo.exists()) { + QImage image(fileInfo.absoluteFilePath()); + if (!image.isNull()) { + image = image.scaled( + requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + if (!image.isNull()) { + then(id, requestedSize, image, fileInfo.absoluteFilePath()); return; } } } mtx::http::ThumbOpts opts; - opts.mxc_url = "mxc://" + m_id.toStdString(); - opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; - opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; + opts.mxc_url = "mxc://" + id.toStdString(); + opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; + opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; opts.method = "crop"; http::client()->get_thumbnail( - opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { + opts, + [fileInfo, requestedSize, then, id](const std::string &res, + mtx::http::RequestErr err) { if (err || res.empty()) { - nhlog::net()->error("Failed to download image {}", - m_id.toStdString()); - m_error = "Failed download"; - emit finished(); + then(id, QSize(), {}, ""); return; } - auto data = QByteArray(res.data(), (int)res.size()); - cache::saveImage(fileName, data); - m_image = utils::readImage(&data); - if (!m_image.isNull()) { - m_image = m_image.scaled( - m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + auto data = QByteArray(res.data(), (int)res.size()); + QImage image = utils::readImage(data); + if (!image.isNull()) { + image = image.scaled( + requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } - m_image.setText("mxc url", "mxc://" + m_id); + image.setText("mxc url", "mxc://" + id); + if (image.save(fileInfo.absoluteFilePath(), "png")) + nhlog::ui()->debug("Wrote: {}", + fileInfo.absoluteFilePath().toStdString()); + else + nhlog::ui()->debug("Failed to write: {}", + fileInfo.absoluteFilePath().toStdString()); - emit finished(); + then(id, requestedSize, image, fileInfo.absoluteFilePath()); }); } else { - auto data = cache::image(m_id); + try { + QString fileName = QString::fromUtf8(id.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); + QFileInfo fileInfo( + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/media_cache", + fileName); + QDir().mkpath(fileInfo.absolutePath()); - if (!data.isNull()) { - m_image = utils::readImage(&data); - m_image.setText("mxc url", "mxc://" + m_id); + if (fileInfo.exists()) { + if (encryptionInfo) { + QFile f(fileInfo.absoluteFilePath()); + f.open(QIODevice::ReadOnly); - if (!m_image.isNull()) { - emit finished(); - return; + QByteArray fileData = f.readAll(); + auto tempData = + mtx::crypto::to_string(mtx::crypto::decrypt_file( + fileData.toStdString(), encryptionInfo.value())); + auto data = + QByteArray(tempData.data(), (int)tempData.size()); + QImage image = utils::readImage(data); + image.setText("mxc url", "mxc://" + id); + if (!image.isNull()) { + then(id, + requestedSize, + image, + fileInfo.absoluteFilePath()); + return; + } + } else { + QImage image(fileInfo.absoluteFilePath()); + if (!image.isNull()) { + then(id, + requestedSize, + image, + fileInfo.absoluteFilePath()); + return; + } + } } + + http::client()->download( + "mxc://" + id.toStdString(), + [fileInfo, requestedSize, then, id, encryptionInfo]( + const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + then(id, QSize(), {}, ""); + return; + } + + auto tempData = res; + QFile f(fileInfo.absoluteFilePath()); + if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) { + then(id, QSize(), {}, ""); + return; + } + f.write(tempData.data(), tempData.size()); + f.close(); + + if (encryptionInfo) { + tempData = + mtx::crypto::to_string(mtx::crypto::decrypt_file( + tempData, encryptionInfo.value())); + auto data = + QByteArray(tempData.data(), (int)tempData.size()); + QImage image = utils::readImage(data); + image.setText("original filename", + QString::fromStdString(originalFilename)); + image.setText("mxc url", "mxc://" + id); + then( + id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } + + QImage image(fileInfo.absoluteFilePath()); + image.setText("original filename", + QString::fromStdString(originalFilename)); + image.setText("mxc url", "mxc://" + id); + image.save(fileInfo.absoluteFilePath()); + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + }); + } catch (std::exception &e) { + nhlog::net()->error("Exception while downloading media: {}", e.what()); } - - http::client()->download( - "mxc://" + m_id.toStdString(), - [this](const std::string &res, - const std::string &, - const std::string &originalFilename, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("Failed to download image {}", - m_id.toStdString()); - m_error = "Failed download"; - emit finished(); - - return; - } - - auto temp = res; - if (m_encryptionInfo) - temp = mtx::crypto::to_string( - mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); - - auto data = QByteArray(temp.data(), (int)temp.size()); - cache::saveImage(m_id, data); - m_image = utils::readImage(&data); - m_image.setText("original filename", - QString::fromStdString(originalFilename)); - m_image.setText("mxc url", "mxc://" + m_id); - - emit finished(); - }); } } diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index f7580bca..7b960836 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -10,21 +10,18 @@ #include #include -#include +#include -#include +#include class MxcImageResponse : public QQuickImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, - const QSize &requestedSize, - boost::optional encryptionInfo) + MxcImageResponse(const QString &id, const QSize &requestedSize) : m_id(id) , m_requestedSize(requestedSize) - , m_encryptionInfo(encryptionInfo) { setAutoDelete(false); } @@ -40,7 +37,6 @@ public: QString m_id, m_error; QSize m_requestedSize; QImage m_image; - boost::optional m_encryptionInfo; }; class MxcImageProvider @@ -50,24 +46,13 @@ class MxcImageProvider Q_OBJECT public slots: QQuickImageResponse *requestImageResponse(const QString &id, - const QSize &requestedSize) override - { - boost::optional info; - auto temp = infos.find("mxc://" + id); - if (temp != infos.end()) - info = *temp; + const QSize &requestedSize) override; - MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); - pool.start(response); - return response; - } - - void addEncryptionInfo(mtx::crypto::EncryptedFile info) - { - infos.insert(QString::fromStdString(info.url), info); - } + static void addEncryptionInfo(mtx::crypto::EncryptedFile info); + static void download(const QString &id, + const QSize &requestedSize, + std::function then); private: QThreadPool pool; - QHash infos; }; diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index d74f9dc9..ea5de674 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include "AvatarProvider.h" diff --git a/src/Utils.cpp b/src/Utils.cpp index 5c03c52f..8a3b9e4c 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -24,6 +24,7 @@ #include "Cache.h" #include "Config.h" +#include "EventAccessors.h" #include "MatrixClient.h" #include "UserSettingsPage.h" @@ -50,6 +51,35 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin ts}; } +RelatedInfo +utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_) +{ + RelatedInfo related = {}; + related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); + related.related_event = std::move(id); + related.type = mtx::accessors::msg_type(event); + + // get body, strip reply fallback, then transform the event to text, if it is a media event + // etc + related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); + QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); + while (related.quoted_body.startsWith(">")) + related.quoted_body.remove(plainQuote); + if (related.quoted_body.startsWith("\n")) + related.quoted_body.remove(0, 1); + related.quoted_body = utils::getQuoteBody(related); + related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room")); + + // get quoted body and strip reply fallback + related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); + related.quoted_formatted_body.remove(QRegularExpression( + ".*", QRegularExpression::DotMatchesEverythingOption)); + related.quoted_formatted_body.replace("@room", "@\u2060aroom"); + related.room = room_id_; + + return related; +} + QString utils::localUser() { @@ -688,11 +718,17 @@ utils::restoreCombobox(QComboBox *combo, const QString &value) } QImage -utils::readImage(const QByteArray *data) +utils::readImage(const QByteArray &data) { QBuffer buf; - buf.setData(*data); + buf.setData(data); QImageReader reader(&buf); reader.setAutoTransform(true); return reader.read(); } + +bool +utils::isReply(const mtx::events::collections::TimelineEvents &e) +{ + return mtx::accessors::relations(e).reply_to().has_value(); +} diff --git a/src/Utils.h b/src/Utils.h index 373bed01..f8ead68c 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -40,6 +40,9 @@ namespace utils { using TimelineEvent = mtx::events::collections::TimelineEvents; +RelatedInfo +stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_); + bool codepointIsEmoji(uint code); @@ -309,5 +312,8 @@ restoreCombobox(QComboBox *combo, const QString &value); //! Read image respecting exif orientation QImage -readImage(const QByteArray *data); +readImage(const QByteArray &data); + +bool +isReply(const mtx::events::collections::TimelineEvents &e); } diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp new file mode 100644 index 00000000..be580b08 --- /dev/null +++ b/src/notifications/Manager.cpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "notifications/Manager.h" + +#include "Cache.h" +#include "EventAccessors.h" +#include "Utils.h" + +QString +NotificationsManager::getMessageTemplate(const mtx::responses::Notification ¬ification) +{ + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + + // TODO: decrypt this message if the decryption setting is on in the UserSettings + if (auto msg = std::get_if>( + ¬ification.event); + msg != nullptr) { + return tr("%1 sent an encrypted message").arg(sender); + } + + if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) { + return tr("* %1 %2", + "Format an emote message in a notification, %1 is the sender, %2 the " + "message") + .arg(sender); + } else if (utils::isReply(notification.event)) { + return tr("%1 replied: %2", + "Format a reply in a notification. %1 is the sender, %2 the message") + .arg(sender); + } else { + return tr("%1: %2", + "Format a normal message in a notification. %1 is the sender, %2 the " + "message") + .arg(sender); + } +} diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h index e2b3236a..416530e0 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h @@ -10,7 +10,12 @@ #include +// convenience definition #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) +#define NHEKO_DBUS_SYS +#endif + +#if defined(NHEKO_DBUS_SYS) #include #include #endif @@ -38,20 +43,51 @@ public: signals: void notificationClicked(const QString roomId, const QString eventId); void sendNotificationReply(const QString roomId, const QString eventId, const QString body); + void systemPostNotificationCb(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon); public slots: void removeNotification(const QString &roomId, const QString &eventId); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) +#if defined(NHEKO_DBUS_SYS) public: void closeNotifications(QString roomId); private: QDBusInterface dbus; + + void systemPostNotification(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon); void closeNotification(uint id); // notification ID to (room ID, event ID) QMap notificationIds; + + const bool hasMarkup_; + const bool hasImages_; +#endif + +#if defined(Q_OS_MACOS) +private: + // Objective-C(++) doesn't like to do lots of regular C++, so the actual notification + // posting is split out + void objCxxPostNotification(const QString &title, + const QString &subtitle, + const QString &informativeText, + const QImage &bodyImage); +#endif + +#if defined(Q_OS_WINDOWS) +private: + void systemPostNotification(const QString &line1, + const QString &line2, + const QString &iconPath); #endif // these slots are platform specific (D-Bus only) @@ -60,9 +96,12 @@ private slots: void actionInvoked(uint id, QString action); void notificationClosed(uint id, uint reason); void notificationReplied(uint id, QString reply); + +private: + QString getMessageTemplate(const mtx::responses::Notification ¬ification); }; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) +#if defined(NHEKO_DBUS_SYS) QDBusArgument & operator<<(QDBusArgument &arg, const QImage &image); const QDBusArgument & diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index a222bd36..598b2bd0 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -1,3 +1,8 @@ +// SPDX-FileCopyrightText: 2012 Roland Hieber +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #include "notifications/Manager.h" #include @@ -7,12 +12,19 @@ #include #include #include +#include +#include +#include + +#include +#include + +#include #include "Cache.h" #include "EventAccessors.h" -#include "MatrixClient.h" +#include "MxcImageProvider.h" #include "Utils.h" -#include NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent) @@ -21,6 +33,18 @@ NotificationsManager::NotificationsManager(QObject *parent) "org.freedesktop.Notifications", QDBusConnection::sessionBus(), this) + , hasMarkup_{std::invoke([this]() -> bool { + for (auto x : dbus.call("GetCapabilities").arguments()) + if (x.toStringList().contains("body-markup")) + return true; + return false; + })} + , hasImages_{std::invoke([this]() -> bool { + for (auto x : dbus.call("GetCapabilities").arguments()) + if (x.toStringList().contains("body-images")) + return true; + return false; + })} { qDBusRegisterMetaType(); @@ -42,12 +66,13 @@ NotificationsManager::NotificationsManager(QObject *parent) "NotificationReplied", this, SLOT(notificationReplied(uint, QString))); -} -// SPDX-FileCopyrightText: 2012 Roland Hieber -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later + connect(this, + &NotificationsManager::systemPostNotificationCb, + this, + &NotificationsManager::systemPostNotification, + Qt::QueuedConnection); +} void NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, @@ -55,25 +80,87 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if { const auto room_id = QString::fromStdString(notification.room_id); const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); - const auto sender = cache::displayName( - room_id, QString::fromStdString(mtx::accessors::sender(notification.event))); - const auto text = utils::event_body(notification.event); + const auto room_name = + QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + auto postNotif = [this, room_id, event_id, room_name, icon](QString text) { + emit systemPostNotificationCb(room_id, event_id, room_name, text, icon); + }; + + QString template_ = getMessageTemplate(notification); + // TODO: decrypt this message if the decryption setting is on in the UserSettings + if (std::holds_alternative>( + notification.event)) { + postNotif(template_); + return; + } + + if (hasMarkup_) { + if (hasImages_ && mtx::accessors::msg_type(notification.event) == + mtx::events::MessageType::Image) { + MxcImageProvider::download( + QString::fromStdString(mtx::accessors::url(notification.event)) + .remove("mxc://"), + QSize(200, 80), + [postNotif, notification, template_]( + QString, QSize, QImage, QString imgPath) { + if (imgPath.isEmpty()) + postNotif(template_ + .arg(utils::stripReplyFallbacks( + notification.event, {}, {}) + .quoted_formatted_body) + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "")); + else + postNotif(template_.arg( + QStringLiteral("
\""")); + }); + return; + } + + postNotif( + template_ + .arg( + utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body) + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "")); + return; + } + + postNotif( + template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body)); +} + +/** + * This function is based on code from + * https://github.com/rohieb/StratumsphereTrayIcon + * Copyright (C) 2012 Roland Hieber + * Licensed under the GNU General Public License, version 3 + */ +void +NotificationsManager::systemPostNotification(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon) +{ QVariantMap hints; hints["image-data"] = icon; hints["sound-name"] = "message-new-instant"; QList argumentList; - argumentList << "nheko"; // app_name - argumentList << (uint)0; // replace_id - argumentList << ""; // app_icon - argumentList << QString::fromStdString( - cache::singleRoomInfo(notification.room_id).name); // summary - - // body - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) - argumentList << "* " + sender + " " + text; - else - argumentList << sender + ": " + text; + argumentList << "nheko"; // app_name + argumentList << (uint)0; // replace_id + argumentList << ""; // app_icon + argumentList << roomName; // summary + argumentList << text; // body // The list of actions has always the action name and then a localized version of that // action. Currently we just use an empty string for that. @@ -84,10 +171,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if argumentList << hints; // hints argumentList << (int)-1; // timeout in ms - static QDBusInterface notifyApp("org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications"); - QDBusPendingCall call = notifyApp.asyncCallWithArgumentList("Notify", argumentList); + QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList); auto watcher = new QDBusPendingCallWatcher{call, this}; connect( watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() { @@ -103,10 +187,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if void NotificationsManager::closeNotification(uint id) { - static QDBusInterface closeCall("org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications"); - auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id + auto call = dbus.asyncCall("CloseNotification", (uint)id); // replace_id auto watcher = new QDBusPendingCallWatcher{call, this}; connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() { if (watcher->reply().type() == QDBusMessage::ErrorMessage) { diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp new file mode 100644 index 00000000..8e36985c --- /dev/null +++ b/src/notifications/ManagerMac.cpp @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Manager.h" + +#include +#include + +#include "Cache.h" +#include "EventAccessors.h" +#include "MxcImageProvider.h" +#include "Utils.h" + +#include + +#include + +static QString +formatNotification(const mtx::responses::Notification ¬ification) +{ + return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body; +} + +void +NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, + const QImage &icon) +{ + Q_UNUSED(icon) + + const auto room_name = + QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + + const auto isEncrypted = + std::get_if>( + ¬ification.event) != nullptr; + const auto isReply = utils::isReply(notification.event); + if (isEncrypted) { + // TODO: decrypt this message if the decryption setting is on in the UserSettings + const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message") + : tr("%1 sent an encrypted message")) + .arg(sender); + objCxxPostNotification(room_name, messageInfo, "", QImage()); + } else { + const QString messageInfo = + (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender); + if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) + MxcImageProvider::download( + QString::fromStdString(mtx::accessors::url(notification.event)) + .remove("mxc://"), + QSize(200, 80), + [this, notification, room_name, messageInfo]( + QString, QSize, QImage image, QString) { + objCxxPostNotification(room_name, + messageInfo, + formatNotification(notification), + image); + }); + else + objCxxPostNotification( + room_name, messageInfo, formatNotification(notification), QImage()); + } +} diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm index 5609d3de..33b7b6af 100644 --- a/src/notifications/ManagerMac.mm +++ b/src/notifications/ManagerMac.mm @@ -1,13 +1,10 @@ #include "notifications/Manager.h" -#include -#include +#import +#import -#include "Cache.h" -#include "EventAccessors.h" -#include "MatrixClient.h" -#include "Utils.h" -#include +#include +#include @interface NSUserNotification (CFIPrivate) - (void)set_identityImage:(NSImage *)image; @@ -19,24 +16,22 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent) } void -NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, - const QImage &icon) +NotificationsManager::objCxxPostNotification(const QString &title, + const QString &subtitle, + const QString &informativeText, + const QImage &bodyImage) { - Q_UNUSED(icon); - const auto sender = cache::displayName(QString::fromStdString(notification.room_id), QString::fromStdString(mtx::accessors::sender(notification.event))); - const auto text = utils::event_body(notification.event); + NSUserNotification *notif = [[NSUserNotification alloc] init]; - NSUserNotification * notif = [[NSUserNotification alloc] init]; - - notif.title = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name).toNSString(); - notif.subtitle = QString("%1 sent a message").arg(sender).toNSString(); - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) - notif.informativeText = QString("* ").append(sender).append(" ").append(text).toNSString(); - else - notif.informativeText = text.toNSString(); + notif.title = title.toNSString(); + notif.subtitle = subtitle.toNSString(); + notif.informativeText = informativeText.toNSString(); notif.soundName = NSUserNotificationDefaultSoundName; + if (!bodyImage.isNull()) + notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage.toCGImage() size: NSZeroSize]; + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif]; [notif autorelease]; } @@ -45,7 +40,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if void NotificationsManager::actionInvoked(uint, QString) { - } +} void NotificationsManager::notificationReplied(uint, QString) diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp index 3152d84f..fe7830a7 100644 --- a/src/notifications/ManagerWin.cpp +++ b/src/notifications/ManagerWin.cpp @@ -5,11 +5,15 @@ #include "notifications/Manager.h" #include "wintoastlib.h" +#include +#include +#include + +#include + #include "Cache.h" #include "EventAccessors.h" -#include "MatrixClient.h" #include "Utils.h" -#include using namespace WinToastLib; @@ -45,34 +49,57 @@ void NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, const QImage &icon) { - Q_UNUSED(icon) - const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); const auto sender = cache::displayName(QString::fromStdString(notification.room_id), QString::fromStdString(mtx::accessors::sender(notification.event))); - const auto text = utils::event_body(notification.event); + const auto isEncrypted = + std::get_if>( + ¬ification.event) != nullptr; + const auto isReply = utils::isReply(notification.event); + + auto formatNotification = [this, notification, sender] { + const auto template_ = getMessageTemplate(notification); + if (std::holds_alternative< + mtx::events::EncryptedEvent>( + notification.event)) { + return template_; + } + + return template_.arg( + utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body); + }; + + const auto line1 = + (room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name); + const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message") + : tr("%1 sent an encrypted message")) + : formatNotification()); + + auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + room_name + "-room-avatar.png"; + if (!icon.save(iconPath)) + iconPath.clear(); + + systemPostNotification(line1, line2, iconPath); +} + +void +NotificationsManager::systemPostNotification(const QString &line1, + const QString &line2, + const QString &iconPath) +{ if (!isInitialized) init(); auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); - if (room_name != sender) - templ.setTextField(QString("%1 - %2").arg(sender).arg(room_name).toStdWString(), - WinToastTemplate::FirstLine); - else - templ.setTextField(QString("%1").arg(sender).toStdWString(), - WinToastTemplate::FirstLine); - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) - templ.setTextField( - QString("* ").append(sender).append(" ").append(text).toStdWString(), - WinToastTemplate::SecondLine); - else - templ.setTextField(QString("%1").arg(text).toStdWString(), - WinToastTemplate::SecondLine); - // TODO: implement room or user avatar - // templ.setImagePath(L"C:/example.png"); + templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine); + templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine); + + if (!iconPath.isNull()) + templ.setImagePath(iconPath.toStdWString()); WinToast::instance()->showToast(templ, new CustomHandler()); } diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 20a3fe10..8a5e4346 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -302,7 +302,9 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown) // NOTE(Nico): rich replies always need a formatted_body! text.format = "org.matrix.custom.html"; - if (ChatPage::instance()->userSettings()->markdown()) + if ((ChatPage::instance()->userSettings()->markdown() && + useMarkdown == MarkdownOverride::NOT_SPECIFIED) || + useMarkdown == MarkdownOverride::ON) text.formatted_body = utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg)) .toStdString(); @@ -572,7 +574,7 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList & auto mimeClass = mime.split("/")[0]; nhlog::ui()->debug("Mime: {}", mime.toStdString()); if (mimeClass == "image") { - QImage img = utils::readImage(&data); + QImage img = utils::readImage(data); dimensions = img.size(); if (img.height() > 200 && img.width() > 360) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 004cf26a..8e96cb3e 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -369,7 +369,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); - bool isReply = relations(event).reply_to().has_value(); + bool isReply = utils::isReply(event); auto formattedBody_ = QString::fromStdString(formatted_body(event)); if (formattedBody_.isEmpty()) { @@ -870,30 +870,7 @@ TimelineModel::relatedInfo(QString id) if (!event) return {}; - RelatedInfo related = {}; - related.quoted_user = QString::fromStdString(mtx::accessors::sender(*event)); - related.related_event = id.toStdString(); - related.type = mtx::accessors::msg_type(*event); - - // get body, strip reply fallback, then transform the event to text, if it is a media event - // etc - related.quoted_body = QString::fromStdString(mtx::accessors::body(*event)); - QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); - while (related.quoted_body.startsWith(">")) - related.quoted_body.remove(plainQuote); - if (related.quoted_body.startsWith("\n")) - related.quoted_body.remove(0, 1); - related.quoted_body = utils::getQuoteBody(related); - related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room")); - - // get quoted body and strip reply fallback - related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(*event); - related.quoted_formatted_body.remove(QRegularExpression( - ".*", QRegularExpression::DotMatchesEverythingOption)); - related.quoted_formatted_body.replace("@room", "@\u2060aroom"); - related.room = room_id_; - - return related; + return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_); } void