summary refs log tree commit diff
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2021-03-17 19:08:17 +0100
committerNicolas Werner <nicolas.werner@hotmail.de>2021-03-17 19:18:07 +0100
commite5d75c814b2175dc37beabff3b0421de59a3e93e (patch)
tree97670bfa02e365ef527ad18fad3d66e16c3ef8d2
parentRefactor image download code to be reusable (diff)
downloadnheko-e5d75c814b2175dc37beabff3b0421de59a3e93e.tar.xz
Clean up notification code a bit
-rw-r--r--src/AvatarProvider.cpp65
-rw-r--r--src/AvatarProvider.h8
-rw-r--r--src/Cache.cpp161
-rw-r--r--src/Cache.h22
-rw-r--r--src/CacheStructs.h1
-rw-r--r--src/Cache_p.h10
-rw-r--r--src/CommunitiesList.cpp36
-rw-r--r--src/MxcImageProvider.cpp18
-rw-r--r--src/Utils.cpp29
-rw-r--r--src/Utils.h3
-rw-r--r--src/notifications/Manager.cpp108
-rw-r--r--src/notifications/Manager.h12
-rw-r--r--src/notifications/ManagerLinux.cpp126
-rw-r--r--src/notifications/ManagerMac.cpp37
-rw-r--r--src/notifications/ManagerWin.cpp23
-rw-r--r--src/timeline/TimelineModel.cpp25
16 files changed, 207 insertions, 477 deletions
diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp
index f64f6859..8cc1144f 100644
--- a/src/AvatarProvider.cpp
+++ b/src/AvatarProvider.cpp
@@ -12,13 +12,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 +34,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;
-        }
-
-        auto proxy = std::make_shared<AvatarProxy>();
-        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);
-                         });
+        MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
+                                   QSize(size, size),
+                                   [callback, cacheKey, recv = QPointer<QObject>(receiver)](
+                                     QString, QSize, QImage img, QString) {
+                                           if (!recv)
+                                                   return;
 
-        mtx::http::ThumbOpts opts;
-        opts.width   = size;
-        opts.height  = size;
-        opts.mxc_url = avatarUrl.toStdString();
+                                           auto proxy = std::make_shared<AvatarProxy>();
+                                           QObject::connect(proxy.get(),
+                                                            &AvatarProxy::avatarDownloaded,
+                                                            recv,
+                                                            [callback, cacheKey](QPixmap pm) {
+                                                                    if (!pm.isNull())
+                                                                            avatar_cache.insert(
+                                                                              cacheKey, pm);
+                                                                    callback(pm);
+                                                            });
 
-        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);
-                  }
+                                           if (img.isNull()) {
+                                                   emit proxy->avatarDownloaded(QPixmap{});
+                                                   return;
+                                           }
 
-                  emit proxy->avatarDownloaded(QByteArray(res.data(), (int)res.size()));
-          });
+                                           auto pm = QPixmap::fromImage(std::move(img));
+                                           emit proxy->avatarDownloaded(pm);
+                                   });
 }
 
 void
@@ -80,8 +69,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 <QPixmap>
 #include <functional>
 
+using AvatarCallback = std::function<void(QPixmap)>;
+
 class AvatarProxy : public QObject
 {
         Q_OBJECT
 
 signals:
-        void avatarDownloaded(const QByteArray &data);
+        void avatarDownloaded(QPixmap pm);
 };
 
-using AvatarCallback = std::function<void(QPixmap)>;
-
 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<std::string>
 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<mtx::events::EventType> &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 <QDateTime>
-#include <QDir>
-#include <QImage>
 #include <QString>
 
 #if __has_include(<lmdbxx/lmdb++.h>)
@@ -135,12 +133,6 @@ hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &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<std::pair<uint64_t, std::string>>
 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<QString, RoomInfo>
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 473c6319..14b13e43 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<uint64_t, std::string, std::greater<uint64_t>>;
         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<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
         std::vector<std::string> 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 db0f72c9..023d0e57 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -9,10 +9,10 @@
 #include <mtxclient/crypto/client.hpp>
 
 #include <QByteArray>
+#include <QDir>
 #include <QFileInfo>
 #include <QStandardPaths>
 
-#include "Cache.h"
 #include "Logging.h"
 #include "MatrixClient.h"
 #include "Utils.h"
@@ -60,12 +60,13 @@ MxcImageProvider::download(const QString &id,
                 QString fileName =
                   QString("%1_%2x%3_crop")
                     .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
-                                                                QByteArray::OmitTrailingEquals)),
-                         requestedSize.width(),
-                         requestedSize.height());
+                                                                QByteArray::OmitTrailingEquals)))
+                    .arg(requestedSize.width())
+                    .arg(requestedSize.height());
                 QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
                                      "/media_cache",
                                    fileName);
+                QDir().mkpath(fileInfo.absolutePath());
 
                 if (fileInfo.exists()) {
                         QImage image(fileInfo.absoluteFilePath());
@@ -102,7 +103,12 @@ MxcImageProvider::download(const QString &id,
                                     requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
                           }
                           image.setText("mxc url", "mxc://" + id);
-                          image.save(fileInfo.absoluteFilePath());
+                          if (image.save(fileInfo.absoluteFilePath(), "png"))
+                                  nhlog::ui()->debug("Wrote: {}",
+                                                     fileInfo.absoluteFilePath().toStdString());
+                          else
+                                  nhlog::ui()->debug("Failed to write: {}",
+                                                     fileInfo.absoluteFilePath().toStdString());
 
                           then(id, requestedSize, image, fileInfo.absoluteFilePath());
                   });
@@ -114,6 +120,7 @@ MxcImageProvider::download(const QString &id,
                           QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
                             "/media_cache",
                           fileName);
+                        QDir().mkpath(fileInfo.absolutePath());
 
                         if (fileInfo.exists()) {
                                 if (encryptionInfo) {
@@ -145,7 +152,6 @@ MxcImageProvider::download(const QString &id,
                                         }
                                 }
                         }
-                        auto data = cache::image(id);
 
                         http::client()->download(
                           "mxc://" + id.toStdString(),
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 4d24c786..8a3b9e4c 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -51,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(
+          "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
+        related.quoted_formatted_body.replace("@room", "@\u2060aroom");
+        related.room = room_id_;
+
+        return related;
+}
+
 QString
 utils::localUser()
 {
diff --git a/src/Utils.h b/src/Utils.h
index eb09172e..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);
 
diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp
index 30e74d33..322213dd 100644
--- a/src/notifications/Manager.cpp
+++ b/src/notifications/Manager.cpp
@@ -2,89 +2,35 @@
 
 #include "Cache.h"
 #include "EventAccessors.h"
-#include "Logging.h"
-#include "MatrixClient.h"
 #include "Utils.h"
 
-#include <QFile>
-#include <QImage>
-#include <QStandardPaths>
-
-#include <mtxclient/crypto/client.hpp>
-
 QString
-NotificationsManager::cacheImage(const mtx::events::collections::TimelineEvents &event)
+NotificationsManager::getMessageTemplate(const mtx::responses::Notification &notification)
 {
-        const auto url      = mtx::accessors::url(event);
-        auto encryptionInfo = mtx::accessors::file(event);
-
-        auto filename = QString::fromStdString(mtx::accessors::body(event));
-        QString path{QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" +
-                     filename};
-
-        bool downloadComplete = false;
-
-        http::client()->download(
-          url,
-          [&downloadComplete, &path, url, encryptionInfo](const std::string &data,
-                                                          const std::string &,
-                                                          const std::string &,
-                                                          mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
-                                             url,
-                                             err->matrix_error.error,
-                                             static_cast<int>(err->status_code));
-                          // the image doesn't exist, so delete the path
-                          path.clear();
-                          downloadComplete = true;
-                          return;
-                  }
-
-                  try {
-                          auto temp = data;
-                          if (encryptionInfo)
-                                  temp = mtx::crypto::to_string(
-                                    mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
-
-                          QFile file{path};
-
-                          if (!file.open(QIODevice::WriteOnly)) {
-                                  path.clear();
-                                  downloadComplete = true;
-                                  return;
-                          }
-
-                          // delete any existing file content
-                          file.resize(0);
-
-                          // resize the image
-                          QImage img{utils::readImage(QByteArray{temp.data()})};
-
-                          if (img.isNull()) {
-                                  path.clear();
-                                  downloadComplete = true;
-                                  return;
-                          }
-
-#ifdef NHEKO_DBUS_SYS // the images in D-Bus notifications are to be 200x100 max
-                          img.scaled(200, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation)
-                            .save(&file);
-#else
-                          img.save(&file);
-#endif // NHEKO_DBUS_SYS
-
-                          file.close();
-
-                          downloadComplete = true;
-                          return;
-                  } catch (const std::exception &e) {
-                          nhlog::ui()->warn("Error while caching file to: {}", e.what());
-                  }
-          });
-
-        while (!downloadComplete)
-                continue;
-
-        return path.toHtmlEscaped();
+        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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+              &notification.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 a1ef9f98..416530e0 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -43,14 +43,15 @@ 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);
 
-private:
-        QString cacheImage(const mtx::events::collections::TimelineEvents &event);
-        QString formatNotification(const mtx::responses::Notification &notification);
-
 #if defined(NHEKO_DBUS_SYS)
 public:
         void closeNotifications(QString roomId);
@@ -95,6 +96,9 @@ 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 &notification);
 };
 
 #if defined(NHEKO_DBUS_SYS)
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index 5581252d..2b0e56e2 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -8,6 +8,7 @@
 #include <QDebug>
 #include <QImage>
 #include <QRegularExpression>
+#include <QStringBuilder>
 #include <QTextDocumentFragment>
 
 #include <functional>
@@ -17,6 +18,7 @@
 
 #include "Cache.h"
 #include "EventAccessors.h"
+#include "MxcImageProvider.h"
 #include "Utils.h"
 
 NotificationsManager::NotificationsManager(QObject *parent)
@@ -59,6 +61,12 @@ NotificationsManager::NotificationsManager(QObject *parent)
                                               "NotificationReplied",
                                               this,
                                               SLOT(notificationReplied(uint, QString)));
+
+        connect(this,
+                &NotificationsManager::systemPostNotificationCb,
+                this,
+                &NotificationsManager::systemPostNotification,
+                Qt::QueuedConnection);
 }
 
 void
@@ -69,9 +77,61 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
         const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
         const auto room_name =
           QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
-        const auto text = formatNotification(notification);
 
-        systemPostNotification(room_id, event_id, room_name, text, icon);
+        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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+              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("<em>", "<i>")
+                                                      .replace("</em>", "</i>")
+                                                      .replace("<strong>", "<b>")
+                                                      .replace("</strong>", "</b>"));
+                                  else
+                                          postNotif(template_.arg(
+                                            QStringLiteral("<br><img src=\"file:///") % imgPath %
+                                            "\" alt=\"" %
+                                            mtx::accessors::formattedBodyWithFallback(
+                                              notification.event) %
+                                            "\">"));
+                          });
+                        return;
+                }
+
+                postNotif(
+                  template_
+                    .arg(
+                      utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body)
+                    .replace("<em>", "<i>")
+                    .replace("</em>", "</i>")
+                    .replace("<strong>", "<b>")
+                    .replace("</strong>", "</b>"));
+                return;
+        }
+
+        postNotif(
+          template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body));
 }
 
 /**
@@ -183,68 +243,6 @@ NotificationsManager::notificationClosed(uint id, uint reason)
 }
 
 /**
- * @param text This should be an HTML-formatted string.
- *
- * If D-Bus says that notifications can have body markup, this function will
- * automatically format the notification to follow the supported HTML subset
- * specified at https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/Markup/
- */
-QString
-NotificationsManager::formatNotification(const mtx::responses::Notification &notification)
-{
-        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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-              &notification.event);
-            msg != nullptr)
-                return tr("%1 sent an encrypted message").arg(sender);
-
-        const auto messageLeadIn =
-          ((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
-             ? "* " + sender + " "
-             : sender +
-                 (utils::isReply(notification.event)
-                    ? tr(" replied",
-                         "Used to denote that this message is a reply to another "
-                         "message. Displayed as 'foo replied: message'.")
-                    : "") +
-                 ": ");
-
-        if (hasMarkup_) {
-                if (hasImages_ && mtx::accessors::msg_type(notification.event) ==
-                                    mtx::events::MessageType::Image) {
-                        QString imgPath = cacheImage(notification.event);
-                        if (imgPath.isNull())
-                                return mtx::accessors::formattedBodyWithFallback(notification.event)
-                                  .prepend(messageLeadIn);
-                        else
-                                return QString("<img src=\"file:///" + imgPath + "\" alt=\"" +
-                                               mtx::accessors::formattedBodyWithFallback(
-                                                 notification.event) +
-                                               "\">")
-                                  .prepend(messageLeadIn);
-                }
-
-                return mtx::accessors::formattedBodyWithFallback(notification.event)
-                  .prepend(messageLeadIn)
-                  .replace("<em>", "<i>")
-                  .replace("</em>", "</i>")
-                  .replace("<strong>", "<b>")
-                  .replace("</strong>", "</b>")
-                  .replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), "");
-        }
-
-        return QTextDocumentFragment::fromHtml(
-                 mtx::accessors::formattedBodyWithFallback(notification.event)
-                   .replace(QRegularExpression("<mx-reply>.+</mx-reply>"), ""))
-          .toPlainText()
-          .prepend(messageLeadIn);
-}
-
-/**
  * Automatic marshaling of a QImage for org.freedesktop.Notifications.Notify
  *
  * This function is from the Clementine project (see
diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp
index de5d0875..3a6becad 100644
--- a/src/notifications/ManagerMac.cpp
+++ b/src/notifications/ManagerMac.cpp
@@ -5,6 +5,7 @@
 
 #include "Cache.h"
 #include "EventAccessors.h"
+#include "MxcImageProvider.h"
 #include "Utils.h"
 
 #include <mtx/responses/notifications.hpp>
@@ -14,17 +15,7 @@
 QString
 NotificationsManager::formatNotification(const mtx::responses::Notification &notification)
 {
-        const auto sender =
-          cache::displayName(QString::fromStdString(notification.room_id),
-                             QString::fromStdString(mtx::accessors::sender(notification.event)));
-
-        return QTextDocumentFragment::fromHtml(
-                 mtx::accessors::formattedBodyWithFallback(notification.event)
-                   .replace(QRegularExpression("<mx-reply>.+</mx-reply>"), ""))
-          .toPlainText()
-          .prepend((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
-                     ? "* " + sender + " "
-                     : "");
+        return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body;
 }
 
 void
@@ -39,25 +30,33 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
           cache::displayName(QString::fromStdString(notification.room_id),
                              QString::fromStdString(mtx::accessors::sender(notification.event)));
 
-        QImage image;
-        if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image)
-                image = QImage{cacheImage(notification.event)};
-
         const auto isEncrypted =
           std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
             &notification.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, "", image);
+                objCxxPostNotification(room_name, messageInfo, "", QImage());
         } else {
                 const QString messageInfo =
                   (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender);
-                objCxxPostNotification(
-                  room_name, messageInfo, formatNotification(notification), image);
+                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/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index baafb6dc..d37bff67 100644
--- a/src/notifications/ManagerWin.cpp
+++ b/src/notifications/ManagerWin.cpp
@@ -108,20 +108,11 @@ NotificationsManager::formatNotification(const mtx::responses::Notification &not
           cache::displayName(QString::fromStdString(notification.room_id),
                              QString::fromStdString(mtx::accessors::sender(notification.event)));
 
-        const auto messageLeadIn =
-          ((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
-             ? "* " + sender + " "
-             : sender +
-                 (utils::isReply(notification.event)
-                    ? tr(" replied",
-                         "Used to denote that this message is a reply to another "
-                         "message. Displayed as 'foo replied: message'.")
-                    : "") +
-                 ": ");
-
-        return QTextDocumentFragment::fromHtml(
-                 mtx::accessors::formattedBodyWithFallback(notification.event)
-                   .replace(QRegularExpression("<mx-reply>.+</mx-reply>"), ""))
-          .toPlainText()
-          .prepend(messageLeadIn);
+        const auto template_ = getMessageTemplate(notification);
+        if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+              notification.event)) {
+                return template_;
+        }
+
+        return template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body);
 }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index cfca626a..8e96cb3e 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -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(
-          "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
-        related.quoted_formatted_body.replace("@room", "@\u2060aroom");
-        related.room = room_id_;
-
-        return related;
+        return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
 }
 
 void