From 33f534c6f85047f00254edbc341b3a7bc1420111 Mon Sep 17 00:00:00 2001
From: Konstantinos Sideris <sideris.konstantin@gmail.com>
Date: Fri, 22 Dec 2017 00:00:48 +0200
Subject: [PATCH] Cache room avatars (#139)

fixes #107
---
 include/Cache.h        |  4 ++++
 include/MatrixClient.h |  5 +++-
 include/RoomList.h     |  5 ++++
 src/Cache.cc           | 52 +++++++++++++++++++++++++++++++++++++++++-
 src/ChatPage.cc        |  4 ++--
 src/MatrixClient.cc    |  4 ++--
 src/RoomList.cc        | 47 +++++++++++++++++++++++++++++++-------
 7 files changed, 107 insertions(+), 14 deletions(-)

diff --git a/include/Cache.h b/include/Cache.h
index c141a42a3..1f6c59f03 100644
--- a/include/Cache.h
+++ b/include/Cache.h
@@ -48,6 +48,9 @@ public:
         bool isFormatValid();
         void setCurrentFormat();
 
+        QByteArray image(const QString &url) const;
+        void saveImage(const QString &url, const QByteArray &data);
+
 private:
         void setNextBatchToken(lmdb::txn &txn, const QString &token);
         void insertRoomState(lmdb::txn &txn, const QString &roomid, const RoomState &state);
@@ -56,6 +59,7 @@ private:
         lmdb::dbi stateDb_;
         lmdb::dbi roomDb_;
         lmdb::dbi invitesDb_;
+        lmdb::dbi imagesDb_;
 
         bool isMounted_;
 
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 2e76061fe..2627f578b 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -98,7 +98,10 @@ signals:
         void fileUploaded(const QString &roomid, const QString &filename, const QString &url);
         void audioUploaded(const QString &roomid, const QString &filename, const QString &url);
 
-        void roomAvatarRetrieved(const QString &roomid, const QPixmap &img);
+        void roomAvatarRetrieved(const QString &roomid,
+                                 const QPixmap &img,
+                                 const QString &url,
+                                 const QByteArray &data);
         void userAvatarRetrieved(const QString &userId, const QImage &img);
         void ownAvatarRetrieved(const QPixmap &img);
         void imageDownloaded(const QString &event_id, const QPixmap &img);
diff --git a/include/RoomList.h b/include/RoomList.h
index 8487df10a..ed05e0bef 100644
--- a/include/RoomList.h
+++ b/include/RoomList.h
@@ -30,6 +30,7 @@
 
 class LeaveRoomDialog;
 class MatrixClient;
+class Cache;
 class OverlayModal;
 class RoomInfoListItem;
 class RoomSettings;
@@ -45,6 +46,7 @@ public:
         RoomList(QSharedPointer<MatrixClient> client, QWidget *parent = 0);
         ~RoomList();
 
+        void setCache(QSharedPointer<Cache> cache) { cache_ = cache; }
         void setInitialRooms(const QMap<QString, QSharedPointer<RoomSettings>> &settings,
                              const QMap<QString, RoomState> &states);
         void sync(const QMap<QString, RoomState> &states,
@@ -52,6 +54,7 @@ public:
         void syncInvites(const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
 
         void clear();
+        void updateAvatar(const QString &room_id, const QString &url);
 
         void addRoom(const QMap<QString, QSharedPointer<RoomSettings>> &settings,
                      const RoomState &state,
@@ -64,6 +67,7 @@ signals:
         void totalUnreadMessageCountUpdated(int count);
         void acceptInvite(const QString &room_id);
         void declineInvite(const QString &room_id);
+        void roomAvatarChanged(const QString &room_id, const QPixmap &img);
 
 public slots:
         void updateRoomAvatar(const QString &roomid, const QPixmap &img);
@@ -96,4 +100,5 @@ private:
         QMap<QString, QSharedPointer<RoomInfoListItem>> rooms_;
 
         QSharedPointer<MatrixClient> client_;
+        QSharedPointer<Cache> cache_;
 };
diff --git a/src/Cache.cc b/src/Cache.cc
index 4e2f32a9d..06e45f13c 100644
--- a/src/Cache.cc
+++ b/src/Cache.cc
@@ -17,6 +17,7 @@
 
 #include <stdexcept>
 
+#include <QByteArray>
 #include <QDebug>
 #include <QFile>
 #include <QStandardPaths>
@@ -34,6 +35,7 @@ Cache::Cache(const QString &userId)
   , stateDb_{0}
   , roomDb_{0}
   , invitesDb_{0}
+  , imagesDb_{0}
   , isMounted_{false}
   , userId_{userId}
 {}
@@ -54,7 +56,7 @@ Cache::setup()
         bool isInitial = !QFile::exists(statePath);
 
         env_ = lmdb::env::create();
-        env_.set_mapsize(128UL * 1024UL * 1024UL); /* 128 MB */
+        env_.set_mapsize(256UL * 1024UL * 1024UL); /* 256 MB */
         env_.set_max_dbs(1024UL);
 
         if (isInitial) {
@@ -91,12 +93,60 @@ Cache::setup()
         stateDb_   = lmdb::dbi::open(txn, "state", MDB_CREATE);
         roomDb_    = lmdb::dbi::open(txn, "rooms", MDB_CREATE);
         invitesDb_ = lmdb::dbi::open(txn, "invites", MDB_CREATE);
+        imagesDb_  = lmdb::dbi::open(txn, "images", MDB_CREATE);
 
         txn.commit();
 
         isMounted_ = true;
 }
 
+void
+Cache::saveImage(const QString &url, const QByteArray &image)
+{
+        if (!isMounted_)
+                return;
+
+        auto key = url.toUtf8();
+
+        try {
+                auto txn = lmdb::txn::begin(env_);
+
+                lmdb::dbi_put(txn,
+                              imagesDb_,
+                              lmdb::val(key.data(), key.size()),
+                              lmdb::val(image.data(), image.size()));
+
+                txn.commit();
+        } catch (const lmdb::error &e) {
+                qCritical() << "saveImage:" << e.what();
+        }
+}
+
+QByteArray
+Cache::image(const QString &url) const
+{
+        auto key = url.toUtf8();
+
+        try {
+                auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+                lmdb::val image;
+
+                bool res = lmdb::dbi_get(txn, imagesDb_, lmdb::val(key.data(), key.size()), image);
+
+                txn.commit();
+
+                if (!res)
+                        return QByteArray();
+
+                return QByteArray(image.data(), image.size());
+        } catch (const lmdb::error &e) {
+                qCritical() << "image:" << e.what();
+        }
+
+        return QByteArray();
+}
+
 void
 Cache::setState(const QString &nextBatchToken, const QMap<QString, RoomState> &states)
 {
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index a5b36d819..8161d62c2 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -232,8 +232,7 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent)
                         view_manager_->queueAudioMessage(roomid, filename, url);
                 });
 
-        connect(
-          client_.data(), &MatrixClient::roomAvatarRetrieved, this, &ChatPage::updateTopBarAvatar);
+        connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
 
         connect(client_.data(),
                 &MatrixClient::initialSyncCompleted,
@@ -353,6 +352,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
         client_->getOwnProfile();
 
         cache_ = QSharedPointer<Cache>(new Cache(userid));
+        room_list_->setCache(cache_);
 
         try {
                 cache_->setup();
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index b5dfe514e..1b2e020d2 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -468,7 +468,7 @@ MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url)
         QNetworkRequest avatar_request(endpoint);
 
         QNetworkReply *reply = get(avatar_request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid]() {
+        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, avatar_url]() {
                 reply->deleteLater();
 
                 int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@@ -486,7 +486,7 @@ MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url)
                 QPixmap pixmap;
                 pixmap.loadFromData(img);
 
-                emit roomAvatarRetrieved(roomid, pixmap);
+                emit roomAvatarRetrieved(roomid, pixmap, avatar_url.toString(), img);
         });
 }
 
diff --git a/src/RoomList.cc b/src/RoomList.cc
index 1e6398384..ea13d7006 100644
--- a/src/RoomList.cc
+++ b/src/RoomList.cc
@@ -15,9 +15,11 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <QBuffer>
 #include <QDebug>
 #include <QObject>
 
+#include "Cache.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "OverlayModal.h"
@@ -53,9 +55,17 @@ RoomList::RoomList(QSharedPointer<MatrixClient> client, QWidget *parent)
         topLayout_->addWidget(scrollArea_);
 
         connect(client_.data(),
-                SIGNAL(roomAvatarRetrieved(const QString &, const QPixmap &)),
+                &MatrixClient::roomAvatarRetrieved,
                 this,
-                SLOT(updateRoomAvatar(const QString &, const QPixmap &)));
+                [=](const QString &room_id,
+                    const QPixmap &img,
+                    const QString &url,
+                    const QByteArray &data) {
+                        if (!cache_.isNull())
+                                cache_->saveImage(url, data);
+
+                        updateRoomAvatar(room_id, img);
+                });
 }
 
 RoomList::~RoomList() {}
@@ -79,12 +89,33 @@ RoomList::addRoom(const QMap<QString, QSharedPointer<RoomSettings>> &settings,
         rooms_.insert(room_id, QSharedPointer<RoomInfoListItem>(room_item));
 
         if (!state.getAvatar().toString().isEmpty())
-                client_->fetchRoomAvatar(room_id, state.getAvatar());
+                updateAvatar(room_id, state.getAvatar().toString());
 
         int pos = contentsLayout_->count() - 1;
         contentsLayout_->insertWidget(pos, room_item);
 }
 
+void
+RoomList::updateAvatar(const QString &room_id, const QString &url)
+{
+        if (url.isEmpty())
+                return;
+
+        QByteArray savedImgData;
+
+        if (!cache_.isNull())
+                savedImgData = cache_->image(url);
+
+        if (savedImgData.isEmpty()) {
+                client_->fetchRoomAvatar(room_id, url);
+        } else {
+                QPixmap img;
+                img.loadFromData(savedImgData);
+
+                updateRoomAvatar(room_id, img);
+        }
+}
+
 void
 RoomList::removeRoom(const QString &room_id, bool reset)
 {
@@ -194,7 +225,7 @@ RoomList::sync(const QMap<QString, RoomState> &states,
                 auto new_avatar     = state.getAvatar();
 
                 if (current_avatar != new_avatar && !new_avatar.toString().isEmpty())
-                        client_->fetchRoomAvatar(room_id, new_avatar);
+                        updateAvatar(room_id, new_avatar.toString());
 
                 room->setState(state);
         }
@@ -246,6 +277,9 @@ RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img)
         }
 
         rooms_.value(roomid)->setAvatar(img.toImage());
+
+        // Used to inform other widgets for the new image data.
+        emit roomAvatarChanged(roomid, img);
 }
 
 void
@@ -308,10 +342,7 @@ RoomList::addInvitedRoom(const QString &room_id, const mtx::responses::InvitedRo
 
         rooms_.insert(room_id, QSharedPointer<RoomInfoListItem>(room_item));
 
-        auto avatarUrl = QString::fromStdString(room.avatar());
-
-        if (!avatarUrl.isEmpty())
-                client_->fetchRoomAvatar(room_id, avatarUrl);
+        updateAvatar(room_id, QString::fromStdString(room.avatar()));
 
         int pos = contentsLayout_->count() - 1;
         contentsLayout_->insertWidget(pos, room_item);
-- 
GitLab