From b89257a34b2a98b737f4ae544f7e436b9000b240 Mon Sep 17 00:00:00 2001
From: Konstantinos Sideris <sideris.konstantin@gmail.com>
Date: Sat, 9 Jun 2018 16:03:14 +0300
Subject: [PATCH] Migrate to mtxclient for the http calls

---
 CMakeLists.txt                         |    4 +-
 deps/CMakeLists.txt                    |    2 +-
 deps/cmake/Olm.cmake                   |    1 +
 deps/cmake/SpdLog.cmake                |    2 +
 include/AvatarProvider.h               |   18 +-
 include/Cache.h                        |    3 +-
 include/ChatPage.h                     |   65 +-
 include/CommunitiesList.h              |    2 +
 include/Logging.hpp                    |   18 +
 include/LoginPage.h                    |   33 +-
 include/MainWindow.h                   |    3 +-
 include/MatrixClient.h                 |  290 +----
 include/RegisterPage.h                 |    5 +
 include/RoomList.h                     |    2 +
 include/TextInputWidget.h              |   10 +
 include/dialogs/ReCaptcha.hpp          |    2 +-
 include/dialogs/RoomSettings.hpp       |    3 +
 include/timeline/TimelineItem.h        |   14 +-
 include/timeline/TimelineView.h        |   65 +-
 include/timeline/TimelineViewManager.h |    6 +-
 include/timeline/widgets/AudioItem.h   |    7 +-
 include/timeline/widgets/FileItem.h    |    7 +-
 include/timeline/widgets/ImageItem.h   |   10 +-
 src/AvatarProvider.cc                  |   50 +-
 src/Cache.cc                           |  112 +-
 src/ChatPage.cc                        |  884 ++++++++++-----
 src/CommunitiesList.cc                 |   61 +-
 src/Logging.cpp                        |   50 +
 src/LoginPage.cc                       |   69 +-
 src/MainWindow.cc                      |   54 +-
 src/MatrixClient.cc                    | 1371 +-----------------------
 src/RegisterPage.cc                    |  130 ++-
 src/RoomList.cc                        |   54 +-
 src/TextInputWidget.cc                 |    2 -
 src/dialogs/ReCaptcha.cpp              |   15 +-
 src/dialogs/RoomSettings.cpp           |   80 +-
 src/main.cc                            |   46 +-
 src/timeline/TimelineItem.cc           |   20 +-
 src/timeline/TimelineView.cc           |  217 +++-
 src/timeline/TimelineViewManager.cc    |   35 +-
 src/timeline/widgets/AudioItem.cc      |   32 +-
 src/timeline/widgets/FileItem.cc       |   44 +-
 src/timeline/widgets/ImageItem.cc      |  140 +--
 src/timeline/widgets/VideoItem.cc      |   16 +-
 44 files changed, 1624 insertions(+), 2430 deletions(-)
 create mode 100644 include/Logging.hpp
 create mode 100644 src/Logging.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 63c718e8d..eedf9a697 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,7 +53,6 @@ include(LMDB)
 # Discover Qt dependencies.
 #
 find_package(Qt5Widgets REQUIRED)
-find_package(Qt5Network REQUIRED)
 find_package(Qt5LinguistTools REQUIRED)
 find_package(Qt5Concurrent REQUIRED)
 find_package(Qt5Svg REQUIRED)
@@ -181,6 +180,7 @@ set(SRC_FILES
     src/Community.cc
     src/InviteeItem.cc
     src/LoginPage.cc
+    src/Logging.cpp
     src/MainWindow.cc
     src/MatrixClient.cc
     src/QuickSwitcher.cc
@@ -287,7 +287,6 @@ qt5_wrap_cpp(MOC_HEADERS
     include/LoginPage.h
     include/MainWindow.h
     include/InviteeItem.h
-    include/MatrixClient.h
     include/QuickSwitcher.h
     include/RegisterPage.h
     include/RoomInfoListItem.h
@@ -314,7 +313,6 @@ set(COMMON_LIBS
     MatrixStructs::MatrixStructs
     MatrixClient::MatrixClient
     Qt5::Widgets
-    Qt5::Network
     Qt5::Svg
     Qt5::Concurrent)
 
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
index 234e904fb..d6bab7e5a 100644
--- a/deps/CMakeLists.txt
+++ b/deps/CMakeLists.txt
@@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs)
 set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de)
 
 set(MTXCLIENT_URL https://github.com/mujx/mtxclient)
-set(MTXCLIENT_TAG 219d2a8887376122e76ba0f64c0cc9935f62f308)
+set(MTXCLIENT_TAG 57f56d1fe73989dbe041a7ac0a28bf2e3286bf98)
 
 set(OLM_URL https://git.matrix.org/git/olm.git)
 set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae)
diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake
index 0eb44d37a..dde18db9e 100644
--- a/deps/cmake/Olm.cmake
+++ b/deps/cmake/Olm.cmake
@@ -15,6 +15,7 @@ ExternalProject_Add(
   CONFIGURE_COMMAND ""
   BUILD_COMMAND ${MAKE_CMD} static
   INSTALL_COMMAND 
+    mkdir -p ${DEPS_INSTALL_DIR}/lib &&
     cp -R ${DEPS_BUILD_DIR}/olm/include ${DEPS_INSTALL_DIR} &&
     cp ${DEPS_BUILD_DIR}/olm/build/libolm.a ${DEPS_INSTALL_DIR}/lib
 )
diff --git a/deps/cmake/SpdLog.cmake b/deps/cmake/SpdLog.cmake
index e49c947f2..1335725e5 100644
--- a/deps/cmake/SpdLog.cmake
+++ b/deps/cmake/SpdLog.cmake
@@ -8,6 +8,8 @@ ExternalProject_Add(
   SOURCE_DIR ${DEPS_BUILD_DIR}/spdlog
   CONFIGURE_COMMAND ${CMAKE_COMMAND}
         -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+        -DSPDLOG_BUILD_EXAMPLES=0
+        -DSPDLOG_BUILD_TESTING=0
         -DCMAKE_BUILD_TYPE=Release
         ${DEPS_BUILD_DIR}/spdlog
 )
diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h
index ce82f2aa5..4b4e15e9d 100644
--- a/include/AvatarProvider.h
+++ b/include/AvatarProvider.h
@@ -20,15 +20,17 @@
 #include <QImage>
 #include <functional>
 
-class AvatarProvider : public QObject
+class AvatarProxy : public QObject
 {
         Q_OBJECT
 
-public:
-        //! The callback is called with the downloaded avatar for the given user
-        //! or the avatar is downloaded first and then saved for re-use.
-        static void resolve(const QString &room_id,
-                            const QString &userId,
-                            QObject *receiver,
-                            std::function<void(QImage)> callback);
+signals:
+        void avatarDownloaded(const QByteArray &data);
 };
+
+using AvatarCallback = std::function<void(QImage)>;
+
+namespace AvatarProvider {
+void
+resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb);
+}
diff --git a/include/Cache.h b/include/Cache.h
index d2574b76c..afc7a148a 100644
--- a/include/Cache.h
+++ b/include/Cache.h
@@ -192,7 +192,7 @@ public:
         void saveState(const mtx::responses::Sync &res);
         bool isInitialized() const;
 
-        QString nextBatchToken() const;
+        std::string nextBatchToken() const;
 
         void deleteData();
 
@@ -237,6 +237,7 @@ public:
         {
                 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);
diff --git a/include/ChatPage.h b/include/ChatPage.h
index b6c431e40..e99e94ba5 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -17,6 +17,8 @@
 
 #pragma once
 
+#include <atomic>
+
 #include <QFrame>
 #include <QHBoxLayout>
 #include <QMap>
@@ -50,9 +52,6 @@ constexpr int CONSENSUS_TIMEOUT      = 1000;
 constexpr int SHOW_CONTENT_TIMEOUT   = 3000;
 constexpr int TYPING_REFRESH_TIMEOUT = 10000;
 
-Q_DECLARE_METATYPE(mtx::responses::Rooms)
-Q_DECLARE_METATYPE(std::vector<std::string>)
-
 class ChatPage : public QWidget
 {
         Q_OBJECT
@@ -71,7 +70,37 @@ public:
         QSharedPointer<UserSettings> userSettings() { return userSettings_; }
         void deleteConfigs();
 
+public slots:
+        void leaveRoom(const QString &room_id);
+
 signals:
+        void connectionLost();
+        void connectionRestored();
+
+        void notificationsRetrieved(const mtx::responses::Notifications &);
+
+        void uploadFailed(const QString &msg);
+        void imageUploaded(const QString &roomid,
+                           const QString &filename,
+                           const QString &url,
+                           const QString &mime,
+                           qint64 dsize);
+        void fileUploaded(const QString &roomid,
+                          const QString &filename,
+                          const QString &url,
+                          const QString &mime,
+                          qint64 dsize);
+        void audioUploaded(const QString &roomid,
+                           const QString &filename,
+                           const QString &url,
+                           const QString &mime,
+                           qint64 dsize);
+        void videoUploaded(const QString &roomid,
+                           const QString &filename,
+                           const QString &url,
+                           const QString &mime,
+                           qint64 dsize);
+
         void contentLoaded();
         void closing();
         void changeWindowTitle(const QString &msg);
@@ -82,30 +111,44 @@ signals:
         void showOverlayProgressBar();
         void startConsesusTimer();
 
+        void removeTimelineEvent(const QString &room_id, const QString &event_id);
+
+        void ownProfileOk();
+        void setUserDisplayName(const QString &name);
+        void setUserAvatar(const QImage &avatar);
+        void loggedOut();
+
+        void trySyncCb();
+        void tryInitialSyncCb();
+        void leftRoom(const QString &room_id);
+
         void initializeRoomList(QMap<QString, RoomInfo>);
         void initializeViews(const mtx::responses::Rooms &rooms);
         void initializeEmptyViews(const std::vector<std::string> &rooms);
         void syncUI(const mtx::responses::Rooms &rooms);
-        void continueSync(const QString &next_batch);
         void syncRoomlist(const std::map<QString, RoomInfo> &updates);
         void syncTopBar(const std::map<QString, RoomInfo> &updates);
+        void dropToLoginPageCb(const QString &msg);
 
 private slots:
         void showUnreadMessageNotification(int count);
         void updateTopBarAvatar(const QString &roomid, const QPixmap &img);
-        void updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name);
         void updateOwnCommunitiesInfo(const QList<QString> &own_communities);
-        void initialSyncCompleted(const mtx::responses::Sync &response);
-        void syncCompleted(const mtx::responses::Sync &response);
         void changeTopRoomInfo(const QString &room_id);
         void logout();
         void removeRoom(const QString &room_id);
-        //! Handles initial sync failures.
-        void retryInitialSync(int status_code = -1);
+        void dropToLoginPage(const QString &msg);
+
+        void joinRoom(const QString &room);
+        void createRoom(const mtx::requests::CreateRoom &req);
+        void sendTypingNotifications();
 
 private:
         static ChatPage *instance_;
 
+        void tryInitialSync();
+        void trySync();
+
         //! Check if the given room is currently open.
         bool isRoomActive(const QString &room_id)
         {
@@ -161,8 +204,8 @@ private:
         // Safety net if consensus is not possible or too slow.
         QTimer *showContentTimer_;
         QTimer *consensusTimer_;
-        QTimer *syncTimeoutTimer_;
-        QTimer *initialSyncTimer_;
+        QTimer connectivityTimer_;
+        std::atomic_bool isConnected_;
 
         QString current_room_;
         QString current_community_;
diff --git a/include/CommunitiesList.h b/include/CommunitiesList.h
index 3299e7c46..78b9602ea 100644
--- a/include/CommunitiesList.h
+++ b/include/CommunitiesList.h
@@ -23,12 +23,14 @@ public:
 
 signals:
         void communityChanged(const QString &id);
+        void avatarRetrieved(const QString &id, const QPixmap &img);
 
 public slots:
         void updateCommunityAvatar(const QString &id, const QPixmap &img);
         void highlightSelectedCommunity(const QString &id);
 
 private:
+        void fetchCommunityAvatar(const QString &id, const QString &avatarUrl);
         void addGlobalItem() { addCommunity(QSharedPointer<Community>(new Community), "world"); }
 
         //! Check whether or not a community id is currently managed.
diff --git a/include/Logging.hpp b/include/Logging.hpp
new file mode 100644
index 000000000..c301d80d9
--- /dev/null
+++ b/include/Logging.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <memory>
+#include <spdlog/spdlog.h>
+
+namespace log {
+void
+init(const std::string &file);
+
+std::shared_ptr<spdlog::logger>
+main();
+
+std::shared_ptr<spdlog::logger>
+net();
+
+std::shared_ptr<spdlog::logger>
+db();
+}
diff --git a/include/LoginPage.h b/include/LoginPage.h
index 34a08df9e..c52ccaa48 100644
--- a/include/LoginPage.h
+++ b/include/LoginPage.h
@@ -28,6 +28,12 @@ class OverlayModal;
 class RaisedButton;
 class TextField;
 
+namespace mtx {
+namespace responses {
+struct Login;
+}
+}
+
 class LoginPage : public QWidget
 {
         Q_OBJECT
@@ -42,12 +48,19 @@ signals:
         void loggingIn();
         void errorOccurred();
 
+        //! Used to trigger the corresponding slot outside of the main thread.
+        void versionErrorCb(const QString &err);
+        void loginErrorCb(const QString &err);
+        void versionOkCb();
+
+        void loginOk(const mtx::responses::Login &res);
+
 protected:
         void paintEvent(QPaintEvent *event) override;
 
 public slots:
         // Displays errors produced during the login.
-        void loginError(QString msg) { error_label_->setText(msg); }
+        void loginError(const QString &msg) { error_label_->setText(msg); }
 
 private slots:
         // Callback for the back button.
@@ -63,13 +76,25 @@ private slots:
         void onServerAddressEntered();
 
         // Callback for errors produced during server probing
-        void versionError(QString error_message);
-
+        void versionError(const QString &error_message);
         // Callback for successful server probing
-        void versionSuccess();
+        void versionOk();
 
 private:
         bool isMatrixIdValid();
+        void checkHomeserverVersion();
+        std::string initialDeviceName()
+        {
+#if defined(Q_OS_MAC)
+                return "nheko on macOS";
+#elif defined(Q_OS_LINUX)
+                return "nheko on Linux";
+#elif defined(Q_OS_WIN)
+                return "nheko on Windows";
+#else
+                return "nheko";
+#endif
+        }
 
         QVBoxLayout *top_layout_;
 
diff --git a/include/MainWindow.h b/include/MainWindow.h
index 0fbc7567e..f0fa9a088 100644
--- a/include/MainWindow.h
+++ b/include/MainWindow.h
@@ -59,6 +59,7 @@ class MainWindow : public QMainWindow
 
 public:
         explicit MainWindow(QWidget *parent = 0);
+        ~MainWindow();
 
         static MainWindow *instance() { return instance_; };
         void saveCurrentWindowSize();
@@ -96,7 +97,7 @@ private slots:
         void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); }
 
         //! Show the chat page and start communicating with the given access token.
-        void showChatPage(QString user_id, QString home_server, QString token);
+        void showChatPage();
 
         void showOverlayProgressBar();
         void removeOverlayProgressBar();
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index eae572819..832d6cad0 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -1,287 +1,25 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 #pragma once
 
-#include <QFileInfo>
-#include <QJsonDocument>
-#include <QNetworkAccessManager>
-#include <QNetworkReply>
-#include <QNetworkRequest>
-#include <QUrl>
-#include <memory>
-#include <mtx.hpp>
-#include <mtx/errors.hpp>
-
-class DownloadMediaProxy : public QObject
-{
-        Q_OBJECT
-
-signals:
-        void imageDownloaded(const QPixmap &data);
-        void fileDownloaded(const QByteArray &data);
-        void avatarDownloaded(const QImage &img);
-};
+#include <QMetaType>
 
-class StateEventProxy : public QObject
-{
-        Q_OBJECT
-
-signals:
-        void stateEventSent();
-        void stateEventError(const QString &msg);
-};
+#include <mtx/responses.hpp>
+#include <mtxclient/http/client.hpp>
 
+Q_DECLARE_METATYPE(mtx::responses::Login)
+Q_DECLARE_METATYPE(mtx::responses::Messages)
+Q_DECLARE_METATYPE(mtx::responses::Notifications)
+Q_DECLARE_METATYPE(mtx::responses::Rooms)
 Q_DECLARE_METATYPE(mtx::responses::Sync)
-
-/*
- * MatrixClient provides the high level API to communicate with
- * a Matrix homeserver. All the responses are returned through signals.
- */
-class MatrixClient : public QNetworkAccessManager
-{
-        Q_OBJECT
-public:
-        MatrixClient(QObject *parent = 0);
-
-        // Client API.
-        void initialSync() noexcept;
-        void sync() noexcept;
-        template<class EventBody, mtx::events::EventType EventT>
-        std::shared_ptr<StateEventProxy> sendStateEvent(const EventBody &body,
-                                                        const QString &roomId,
-                                                        const QString &stateKey = "");
-        void sendRoomMessage(mtx::events::MessageType ty,
-                             int txnId,
-                             const QString &roomid,
-                             const QString &msg,
-                             const QString &mime,
-                             uint64_t media_size,
-                             const QString &url = "") noexcept;
-        void login(const QString &username, const QString &password) noexcept;
-        void registerUser(const QString &username,
-                          const QString &password,
-                          const QString &server,
-                          const QString &session = "") noexcept;
-        void versions() noexcept;
-        void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url);
-        //! Download user's avatar.
-        QSharedPointer<DownloadMediaProxy> fetchUserAvatar(const QUrl &avatarUrl);
-        void fetchCommunityAvatar(const QString &communityId, const QUrl &avatarUrl);
-        void fetchCommunityProfile(const QString &communityId);
-        void fetchCommunityRooms(const QString &communityId);
-        QSharedPointer<DownloadMediaProxy> downloadImage(const QUrl &url);
-        QSharedPointer<DownloadMediaProxy> downloadFile(const QUrl &url);
-        void messages(const QString &room_id, const QString &from_token, int limit = 30) noexcept;
-        void uploadImage(const QString &roomid,
-                         const QString &filename,
-                         const QSharedPointer<QIODevice> data);
-        void uploadFile(const QString &roomid,
-                        const QString &filename,
-                        const QSharedPointer<QIODevice> data);
-        void uploadAudio(const QString &roomid,
-                         const QString &filename,
-                         const QSharedPointer<QIODevice> data);
-        void uploadVideo(const QString &roomid,
-                         const QString &filename,
-                         const QSharedPointer<QIODevice> data);
-        void uploadFilter(const QString &filter) noexcept;
-        void joinRoom(const QString &roomIdOrAlias);
-        void leaveRoom(const QString &roomId);
-        void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000);
-        void removeTypingNotification(const QString &roomid);
-        void readEvent(const QString &room_id, const QString &event_id);
-        void redactEvent(const QString &room_id, const QString &event_id);
-        void inviteUser(const QString &room_id, const QString &user);
-        void createRoom(const mtx::requests::CreateRoom &request);
-        void getNotifications() noexcept;
-
-        QUrl getHomeServer() { return server_; };
-        int transactionId() { return txn_id_; };
-        int incrementTransactionId() { return ++txn_id_; };
-
-        void reset() noexcept;
-
-public slots:
-        void getOwnProfile() noexcept;
-        void getOwnCommunities() noexcept;
-        void logout() noexcept;
-
-        void setServer(const QString &server)
-        {
-                server_ = QUrl(QString("%1://%2").arg(serverProtocol_).arg(server));
-        };
-        void setAccessToken(const QString &token) { token_ = token; };
-        void setNextBatchToken(const QString &next_batch) { next_batch_ = next_batch; };
-
-signals:
-        void loginError(const QString &error);
-        void registerError(const QString &error);
-        void registrationFlow(const QString &user,
-                              const QString &pass,
-                              const QString &server,
-                              const QString &session);
-        void versionError(const QString &error);
-
-        void loggedOut();
-        void invitedUser(const QString &room_id, const QString &user);
-        void roomCreated(const QString &room_id);
-
-        void loginSuccess(const QString &userid, const QString &homeserver, const QString &token);
-        void registerSuccess(const QString &userid,
-                             const QString &homeserver,
-                             const QString &token);
-        void versionSuccess();
-        void uploadFailed(int statusCode, const QString &msg);
-        void imageUploaded(const QString &roomid,
-                           const QString &filename,
-                           const QString &url,
-                           const QString &mime,
-                           uint64_t size);
-        void fileUploaded(const QString &roomid,
-                          const QString &filename,
-                          const QString &url,
-                          const QString &mime,
-                          uint64_t size);
-        void audioUploaded(const QString &roomid,
-                           const QString &filename,
-                           const QString &url,
-                           const QString &mime,
-                           uint64_t size);
-        void videoUploaded(const QString &roomid,
-                           const QString &filename,
-                           const QString &url,
-                           const QString &mime,
-                           uint64_t size);
-        void roomAvatarRetrieved(const QString &roomid,
-                                 const QPixmap &img,
-                                 const QString &url,
-                                 const QByteArray &data);
-        void userAvatarRetrieved(const QString &userId, const QImage &img);
-        void communityAvatarRetrieved(const QString &communityId, const QPixmap &img);
-        void communityProfileRetrieved(const QString &communityId, const QJsonObject &profile);
-        void communityRoomsRetrieved(const QString &communityId, const QJsonObject &rooms);
-
-        // Returned profile data for the user's account.
-        void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name);
-        void getOwnCommunitiesResponse(const QList<QString> &own_communities);
-        void initialSyncCompleted(const mtx::responses::Sync &response);
-        void initialSyncFailed(int status_code = -1);
-        void syncCompleted(const mtx::responses::Sync &response);
-        void syncFailed(const QString &msg);
-        void joinFailed(const QString &msg);
-        void messageSent(const QString &event_id, const QString &roomid, int txn_id);
-        void messageSendFailed(const QString &roomid, int txn_id);
-        void emoteSent(const QString &event_id, const QString &roomid, int txn_id);
-        void messagesRetrieved(const QString &room_id, const mtx::responses::Messages &msgs);
-        void joinedRoom(const QString &room_id);
-        void leftRoom(const QString &room_id);
-        void roomCreationFailed(const QString &msg);
-
-        void redactionFailed(const QString &error);
-        void redactionCompleted(const QString &room_id, const QString &event_id);
-        void invalidToken();
-        void syncError(const QString &error);
-        void notificationsRetrieved(const mtx::responses::Notifications &notifications);
-
-private:
-        QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev);
-        QJsonObject getUploadReply(QNetworkReply *reply);
-        void setupAuth(QNetworkRequest &req)
-        {
-                req.setRawHeader("Authorization", QString("Bearer %1").arg(token_).toLocal8Bit());
-        }
-
-        // Client API prefix.
-        QString clientApiUrl_;
-
-        // Media API prefix.
-        QString mediaApiUrl_;
-
-        // The Matrix server used for communication.
-        QUrl server_;
-
-        // The access token used for authentication.
-        QString token_;
-
-        // Increasing transaction ID.
-        int txn_id_;
-
-        //! Token to be used for the next sync.
-        QString next_batch_;
-        //! http or https (default).
-        QString serverProtocol_;
-        //! Filter to be send as filter-param for (initial) /sync requests.
-        QString filter_;
-};
+Q_DECLARE_METATYPE(std::string)
+Q_DECLARE_METATYPE(std::vector<std::string>);
 
 namespace http {
-//! Initialize the http module
-void
-init();
-
-//! Retrieve the client instance.
-MatrixClient *
+namespace v2 {
+mtx::http::Client *
 client();
 }
 
-template<class EventBody, mtx::events::EventType EventT>
-std::shared_ptr<StateEventProxy>
-MatrixClient::sendStateEvent(const EventBody &body, const QString &roomId, const QString &stateKey)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/state/%2/%3")
-                                           .arg(roomId)
-                                           .arg(QString::fromStdString(to_string(EventT)))
-                                           .arg(stateKey));
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        auto proxy = std::shared_ptr<StateEventProxy>(new StateEventProxy,
-                                                      [](StateEventProxy *p) { p->deleteLater(); });
-
-        auto serializedBody = nlohmann::json(body).dump();
-        auto reply = put(request, QByteArray(serializedBody.data(), serializedBody.size()));
-        connect(reply, &QNetworkReply::finished, this, [reply, proxy]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-                auto data  = reply->readAll();
-
-                if (status == 0 || status >= 400) {
-                        try {
-                                mtx::errors::Error res = nlohmann::json::parse(data);
-                                emit proxy->stateEventError(QString::fromStdString(res.error));
-                        } catch (const std::exception &e) {
-                                emit proxy->stateEventError(QString::fromStdString(e.what()));
-                        }
-
-                        return;
-                }
-
-                try {
-                        mtx::responses::EventId res = nlohmann::json::parse(data);
-                        emit proxy->stateEventSent();
-                } catch (const std::exception &e) {
-                        emit proxy->stateEventError(QString::fromStdString(e.what()));
-                }
-        });
-
-        return proxy;
+//! Initialize the http module
+void
+init();
 }
diff --git a/include/RegisterPage.h b/include/RegisterPage.h
index f4d978169..d02de7c46 100644
--- a/include/RegisterPage.h
+++ b/include/RegisterPage.h
@@ -44,6 +44,11 @@ signals:
         void backButtonClicked();
         void errorOccurred();
         void registering();
+        void registerOk();
+        void registerErrorCb(const QString &msg);
+        void registrationFlow(const std::string &user,
+                              const std::string &pass,
+                              const std::string &session);
 
 private slots:
         void onBackButtonClicked();
diff --git a/include/RoomList.h b/include/RoomList.h
index 98d9443e5..59b0e865d 100644
--- a/include/RoomList.h
+++ b/include/RoomList.h
@@ -60,6 +60,8 @@ signals:
         void acceptInvite(const QString &room_id);
         void declineInvite(const QString &room_id);
         void roomAvatarChanged(const QString &room_id, const QPixmap &img);
+        void joinRoom(const QString &room_id);
+        void updateRoomAvatarCb(const QString &room_id, const QPixmap &img);
 
 public slots:
         void updateRoomAvatar(const QString &roomid, const QPixmap &img);
diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h
index c679b9b2e..af58c2c3b 100644
--- a/include/TextInputWidget.h
+++ b/include/TextInputWidget.h
@@ -129,6 +129,16 @@ public:
 
         QColor borderColor() const { return borderColor_; }
         void setBorderColor(QColor &color) { borderColor_ = color; }
+        void disableInput()
+        {
+                input_->setEnabled(false);
+                input_->setPlaceholderText(tr("Connection lost. Nheko is trying to re-connect..."));
+        }
+        void enableInput()
+        {
+                input_->setEnabled(true);
+                input_->setPlaceholderText(tr("Write a message..."));
+        }
 
 public slots:
         void openFileSelection();
diff --git a/include/dialogs/ReCaptcha.hpp b/include/dialogs/ReCaptcha.hpp
index 1eda40c7d..5f47b0ebb 100644
--- a/include/dialogs/ReCaptcha.hpp
+++ b/include/dialogs/ReCaptcha.hpp
@@ -12,7 +12,7 @@ class ReCaptcha : public QWidget
         Q_OBJECT
 
 public:
-        ReCaptcha(const QString &server, const QString &session, QWidget *parent = nullptr);
+        ReCaptcha(const QString &session, QWidget *parent = nullptr);
 
 protected:
         void paintEvent(QPaintEvent *event) override;
diff --git a/include/dialogs/RoomSettings.hpp b/include/dialogs/RoomSettings.hpp
index 375a531ee..9a01d5c9f 100644
--- a/include/dialogs/RoomSettings.hpp
+++ b/include/dialogs/RoomSettings.hpp
@@ -30,6 +30,9 @@ public:
 
 signals:
         void nameChanged(const QString &roomName);
+        void nameEventSentCb(const QString &newName);
+        void topicEventSentCb();
+        void stateEventErrorCb(const QString &msg);
 
 private:
         QString roomId_;
diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h
index 9997ec1db..4dcca1a5a 100644
--- a/include/timeline/TimelineItem.h
+++ b/include/timeline/TimelineItem.h
@@ -197,12 +197,24 @@ public:
         void sendReadReceipt() const
         {
                 if (!event_id_.isEmpty())
-                        http::client()->readEvent(room_id_, event_id_);
+                        http::v2::client()->read_event(
+                          room_id_.toStdString(),
+                          event_id_.toStdString(),
+                          [this](mtx::http::RequestErr err) {
+                                  if (err) {
+                                          qWarning() << QString("failed to read_event (%1, %2)")
+                                                          .arg(room_id_, event_id_);
+                                  }
+                          });
         }
 
         //! Add a user avatar for this event.
         void addAvatar();
 
+signals:
+        void eventRedacted(const QString &event_id);
+        void redactionFailed(const QString &msg);
+
 protected:
         void paintEvent(QPaintEvent *event) override;
         void contextMenuEvent(QContextMenuEvent *event) override;
diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h
index e6e35ccbb..30af97fb3 100644
--- a/include/timeline/TimelineView.h
+++ b/include/timeline/TimelineView.h
@@ -18,7 +18,6 @@
 #pragma once
 
 #include <QApplication>
-#include <QDebug>
 #include <QLayout>
 #include <QList>
 #include <QQueue>
@@ -42,31 +41,13 @@ struct DescInfo;
 struct PendingMessage
 {
         mtx::events::MessageType ty;
-        int txn_id;
+        std::string txn_id;
         QString body;
         QString filename;
         QString mime;
         uint64_t media_size;
         QString event_id;
         TimelineItem *widget;
-
-        PendingMessage(mtx::events::MessageType ty,
-                       int txn_id,
-                       QString body,
-                       QString filename,
-                       QString mime,
-                       uint64_t media_size,
-                       QString event_id,
-                       TimelineItem *widget)
-          : ty(ty)
-          , txn_id(txn_id)
-          , body(body)
-          , filename(filename)
-          , mime(mime)
-          , media_size(media_size)
-          , event_id(event_id)
-          , widget(widget)
-        {}
 };
 
 // In which place new TimelineItems should be inserted.
@@ -129,7 +110,7 @@ public:
                             const QString &filename,
                             const QString &mime,
                             uint64_t size);
-        void updatePendingMessage(int txn_id, QString event_id);
+        void updatePendingMessage(const std::string &txn_id, const QString &event_id);
         void scrollDown();
         QLabel *createDateSeparator(QDateTime datetime);
 
@@ -142,18 +123,21 @@ public slots:
         void fetchHistory();
 
         // Add old events at the top of the timeline.
-        void addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs);
+        void addBackwardsEvents(const mtx::responses::Messages &msgs);
 
         // Whether or not the initial batch has been loaded.
         bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; }
 
-        void handleFailedMessage(int txnid);
+        void handleFailedMessage(const std::string &txn_id);
 
 private slots:
         void sendNextPendingMessage();
 
 signals:
         void updateLastTimelineMessage(const QString &user, const DescInfo &info);
+        void messagesRetrieved(const mtx::responses::Messages &res);
+        void messageFailed(const std::string &txn_id);
+        void messageSent(const std::string &txn_id, const QString &event_id);
 
 protected:
         void paintEvent(QPaintEvent *event) override;
@@ -165,6 +149,13 @@ private:
 
         QWidget *relativeWidget(TimelineItem *item, int dt) const;
 
+        //! Callback for all message sending.
+        void sendRoomMessageHandler(const std::string &txn_id,
+                                    const mtx::responses::EventId &res,
+                                    mtx::http::RequestErr err);
+
+        //! Call the /messages endpoint to fill the timeline.
+        void getMessages();
         //! HACK: Fixing layout flickering when adding to the bottom
         //! of the timeline.
         void pushTimelineItem(TimelineItem *item)
@@ -230,8 +221,10 @@ private:
                               uint64_t origin_server_ts,
                               TimelineDirection direction);
 
-        bool isPendingMessage(const QString &txnid, const QString &sender, const QString &userid);
-        void removePendingMessage(const QString &txnid);
+        bool isPendingMessage(const std::string &txn_id,
+                              const QString &sender,
+                              const QString &userid);
+        void removePendingMessage(const std::string &txn_id);
 
         bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); }
 
@@ -320,9 +313,15 @@ TimelineView::addUserMessage(const QString &url,
         // Keep track of the sender and the timestamp of the current message.
         saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
 
-        int txn_id = http::client()->incrementTransactionId();
+        PendingMessage message;
+        message.ty         = MsgType;
+        message.txn_id     = mtx::client::utils::random_token();
+        message.body       = url;
+        message.filename   = trimmed;
+        message.mime       = mime;
+        message.media_size = size;
+        message.widget     = view_item;
 
-        PendingMessage message(MsgType, txn_id, url, trimmed, mime, size, "", view_item);
         handleNewUserMessage(message);
 }
 
@@ -351,10 +350,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
         const auto event_id = QString::fromStdString(event.event_id);
         const auto sender   = QString::fromStdString(event.sender);
 
-        const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
-        if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
+        const auto txn_id = event.unsigned_data.transaction_id;
+        if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
             isDuplicate(event_id)) {
-                removePendingMessage(txnid);
+                removePendingMessage(txn_id);
                 return nullptr;
         }
 
@@ -376,10 +375,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
         const auto event_id = QString::fromStdString(event.event_id);
         const auto sender   = QString::fromStdString(event.sender);
 
-        const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
-        if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
+        const auto txn_id = event.unsigned_data.transaction_id;
+        if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
             isDuplicate(event_id)) {
-                removePendingMessage(txnid);
+                removePendingMessage(txn_id);
                 return nullptr;
         }
 
diff --git a/include/timeline/TimelineViewManager.h b/include/timeline/TimelineViewManager.h
index 308b83aa0..9e31ecbf1 100644
--- a/include/timeline/TimelineViewManager.h
+++ b/include/timeline/TimelineViewManager.h
@@ -56,6 +56,8 @@ signals:
         void updateRoomsLastMessage(const QString &user, const DescInfo &info);
 
 public slots:
+        void removeTimelineEvent(const QString &room_id, const QString &event_id);
+
         void setHistoryView(const QString &room_id);
         void queueTextMessage(const QString &msg);
         void queueEmoteMessage(const QString &msg);
@@ -80,10 +82,6 @@ public slots:
                                const QString &mime,
                                uint64_t dsize);
 
-private slots:
-        void messageSent(const QString &eventid, const QString &roomid, int txnid);
-        void messageSendFailed(const QString &roomid, int txnid);
-
 private:
         //! Check if the given room id is managed by a TimelineView.
         bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }
diff --git a/include/timeline/widgets/AudioItem.h b/include/timeline/widgets/AudioItem.h
index b31385d1c..7b0781a25 100644
--- a/include/timeline/widgets/AudioItem.h
+++ b/include/timeline/widgets/AudioItem.h
@@ -69,9 +69,14 @@ protected:
         void resizeEvent(QResizeEvent *event) override;
         void mousePressEvent(QMouseEvent *event) override;
 
+signals:
+        void fileDownloadedCb(const QByteArray &data);
+
+private slots:
+        void fileDownloaded(const QByteArray &data);
+
 private:
         void init();
-        void fileDownloaded(const QByteArray &data);
 
         enum class AudioState
         {
diff --git a/include/timeline/widgets/FileItem.h b/include/timeline/widgets/FileItem.h
index 09181d324..66543e791 100644
--- a/include/timeline/widgets/FileItem.h
+++ b/include/timeline/widgets/FileItem.h
@@ -52,15 +52,20 @@ public:
         QColor iconColor() const { return iconColor_; }
         QColor backgroundColor() const { return backgroundColor_; }
 
+signals:
+        void fileDownloadedCb(const QByteArray &data);
+
 protected:
         void paintEvent(QPaintEvent *event) override;
         void mousePressEvent(QMouseEvent *event) override;
         void resizeEvent(QResizeEvent *event) override;
 
+private slots:
+        void fileDownloaded(const QByteArray &data);
+
 private:
         void openUrl();
         void init();
-        void fileDownloaded(const QByteArray &data);
 
         QUrl url_;
         QString text_;
diff --git a/include/timeline/widgets/ImageItem.h b/include/timeline/widgets/ImageItem.h
index b17b2d8b8..e9d823f43 100644
--- a/include/timeline/widgets/ImageItem.h
+++ b/include/timeline/widgets/ImageItem.h
@@ -40,13 +40,17 @@ public:
                   uint64_t size,
                   QWidget *parent = nullptr);
 
-        void setImage(const QPixmap &image);
-
         QSize sizeHint() const override;
 
 public slots:
         //! Show a save as dialog for the image.
         void saveAs();
+        void setImage(const QPixmap &image);
+        void saveImage(const QString &filename, const QByteArray &data);
+
+signals:
+        void imageDownloaded(const QPixmap &img);
+        void imageSaved(const QString &filename, const QByteArray &data);
 
 protected:
         void paintEvent(QPaintEvent *event) override;
@@ -57,7 +61,9 @@ protected:
         bool isInteractive_ = true;
 
 private:
+        void init();
         void openUrl();
+        void downloadMedia(const QUrl &url);
 
         int max_width_  = 500;
         int max_height_ = 300;
diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc
index 49e52a827..ad095023c 100644
--- a/src/AvatarProvider.cc
+++ b/src/AvatarProvider.cc
@@ -16,17 +16,17 @@
  */
 
 #include <QBuffer>
-#include <QtConcurrent>
+#include <memory>
 
 #include "AvatarProvider.h"
 #include "Cache.h"
+#include "Logging.hpp"
 #include "MatrixClient.h"
 
+namespace AvatarProvider {
+
 void
-AvatarProvider::resolve(const QString &room_id,
-                        const QString &user_id,
-                        QObject *receiver,
-                        std::function<void(QImage)> callback)
+resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback)
 {
         const auto key       = QString("%1 %2").arg(room_id).arg(user_id);
         const auto avatarUrl = Cache::avatarUrl(room_id, user_id);
@@ -43,24 +43,30 @@ AvatarProvider::resolve(const QString &room_id,
                 return;
         }
 
-        auto proxy = http::client()->fetchUserAvatar(avatarUrl);
+        auto proxy = std::make_shared<AvatarProxy>();
+        QObject::connect(proxy.get(),
+                         &AvatarProxy::avatarDownloaded,
+                         receiver,
+                         [callback](const QByteArray &data) { callback(QImage::fromData(data)); });
 
-        if (proxy.isNull())
-                return;
+        mtx::http::ThumbOpts opts;
+        opts.mxc_url = avatarUrl.toStdString();
 
-        connect(proxy.data(),
-                &DownloadMediaProxy::avatarDownloaded,
-                receiver,
-                [user_id, proxy, callback, avatarUrl](const QImage &img) {
-                        proxy->deleteLater();
-                        QtConcurrent::run([img, avatarUrl]() {
-                                QByteArray data;
-                                QBuffer buffer(&data);
-                                buffer.open(QIODevice::WriteOnly);
-                                img.save(&buffer, "PNG");
+        http::v2::client()->get_thumbnail(
+          opts,
+          [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          log::net()->warn("failed to download avatar: {} - ({} {})",
+                                           opts.mxc_url,
+                                           mtx::errors::to_string(err->matrix_error.errcode),
+                                           err->matrix_error.error);
+                          return;
+                  }
 
-                                cache::client()->saveImage(avatarUrl, data);
-                        });
-                        callback(img);
-                });
+                  cache::client()->saveImage(opts.mxc_url, res);
+
+                  auto data = QByteArray(res.data(), res.size());
+                  emit proxy->avatarDownloaded(data);
+          });
+}
 }
diff --git a/src/Cache.cc b/src/Cache.cc
index c055ab059..2a5554250 100644
--- a/src/Cache.cc
+++ b/src/Cache.cc
@@ -19,7 +19,6 @@
 #include <stdexcept>
 
 #include <QByteArray>
-#include <QDebug>
 #include <QFile>
 #include <QHash>
 #include <QStandardPaths>
@@ -27,6 +26,7 @@
 #include <variant.hpp>
 
 #include "Cache.h"
+#include "Logging.hpp"
 #include "Utils.h"
 
 //! Should be changed when a breaking change occurs in the cache format.
@@ -62,6 +62,14 @@ namespace cache {
 void
 init(const QString &user_id)
 {
+        qRegisterMetaType<SearchResult>();
+        qRegisterMetaType<QVector<SearchResult>>();
+        qRegisterMetaType<RoomMember>();
+        qRegisterMetaType<RoomSearchResult>();
+        qRegisterMetaType<RoomInfo>();
+        qRegisterMetaType<QMap<QString, RoomInfo>>();
+        qRegisterMetaType<std::map<QString, RoomInfo>>();
+
         if (!instance_)
                 instance_ = std::make_unique<Cache>(user_id);
 }
@@ -88,7 +96,7 @@ Cache::Cache(const QString &userId, QObject *parent)
 void
 Cache::setup()
 {
-        qDebug() << "Setting up cache";
+        log::db()->debug("setting up cache");
 
         auto statePath = QString("%1/%2/state")
                            .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
@@ -105,7 +113,7 @@ Cache::setup()
         env_.set_max_dbs(1024UL);
 
         if (isInitial) {
-                qDebug() << "First time initializing LMDB";
+                log::db()->info("initializing LMDB");
 
                 if (!QDir().mkpath(statePath)) {
                         throw std::runtime_error(
@@ -121,7 +129,7 @@ Cache::setup()
                                                  std::string(e.what()));
                 }
 
-                qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what();
+                log::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
 
                 QDir stateDir(statePath);
 
@@ -142,29 +150,34 @@ Cache::setup()
         readReceiptsDb_  = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
         notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
         txn.commit();
-
-        qRegisterMetaType<RoomInfo>();
 }
 
 void
-Cache::saveImage(const QString &url, const QByteArray &image)
+Cache::saveImage(const std::string &url, const std::string &img_data)
 {
-        auto key = url.toUtf8();
+        if (url.empty() || img_data.empty())
+                return;
 
         try {
                 auto txn = lmdb::txn::begin(env_);
 
                 lmdb::dbi_put(txn,
                               mediaDb_,
-                              lmdb::val(key.data(), key.size()),
-                              lmdb::val(image.data(), image.size()));
+                              lmdb::val(url.data(), url.size()),
+                              lmdb::val(img_data.data(), img_data.size()));
 
                 txn.commit();
         } catch (const lmdb::error &e) {
-                qCritical() << "saveImage:" << e.what();
+                log::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) const
 {
@@ -180,7 +193,7 @@ Cache::image(lmdb::txn &txn, const std::string &url) const
 
                 return QByteArray(image.data(), image.size());
         } catch (const lmdb::error &e) {
-                qCritical() << "image:" << e.what() << QString::fromStdString(url);
+                log::db()->critical("image: {}, {}", e.what(), url);
         }
 
         return QByteArray();
@@ -208,7 +221,7 @@ Cache::image(const QString &url) const
 
                 return QByteArray(image.data(), image.size());
         } catch (const lmdb::error &e) {
-                qCritical() << "image:" << e.what() << url;
+                log::db()->critical("image: {} {}", e.what(), url.toStdString());
         }
 
         return QByteArray();
@@ -271,7 +284,7 @@ Cache::isInitialized() const
         return res;
 }
 
-QString
+std::string
 Cache::nextBatchToken() const
 {
         auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
@@ -281,13 +294,13 @@ Cache::nextBatchToken() const
 
         txn.commit();
 
-        return QString::fromUtf8(token.data(), token.size());
+        return std::string(token.data(), token.size());
 }
 
 void
 Cache::deleteData()
 {
-        qInfo() << "Deleting cache data";
+        log::db()->info("deleting data");
 
         if (!cacheDirectory_.isEmpty())
                 QDir(cacheDirectory_).removeRecursively();
@@ -309,8 +322,9 @@ Cache::isFormatValid()
         std::string stored_version(current_version.data(), current_version.size());
 
         if (stored_version != CURRENT_CACHE_FORMAT_VERSION) {
-                qWarning() << "Stored format version" << QString::fromStdString(stored_version);
-                qWarning() << "There are breaking changes in the cache format.";
+                log::db()->warn("breaking changes in the cache format. stored: {}, current: {}",
+                                stored_version,
+                                CURRENT_CACHE_FORMAT_VERSION);
                 return false;
         }
 
@@ -360,7 +374,7 @@ Cache::readReceipts(const QString &event_id, const QString &room_id)
                 }
 
         } catch (const lmdb::error &e) {
-                qCritical() << "readReceipts:" << e.what();
+                log::db()->critical("readReceipts: {}", e.what());
         }
 
         return receipts;
@@ -410,7 +424,7 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei
                                       lmdb::val(merged_receipts.data(), merged_receipts.size()));
 
                 } catch (const lmdb::error &e) {
-                        qCritical() << "updateReadReceipts:" << e.what();
+                        log::db()->critical("updateReadReceipts: {}", e.what());
                 }
         }
 }
@@ -568,9 +582,9 @@ Cache::singleRoomInfo(const std::string &room_id)
 
                         return tmp;
                 } catch (const json::exception &e) {
-                        qWarning()
-                          << "failed to parse room info:" << QString::fromStdString(room_id)
-                          << QString::fromStdString(std::string(data.data(), data.size()));
+                        log::db()->warn("failed to parse room info: room_id ({}), {}",
+                                        room_id,
+                                        std::string(data.data(), data.size()));
                 }
         }
 
@@ -600,9 +614,9 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
 
                                 room_info.emplace(QString::fromStdString(room), std::move(tmp));
                         } catch (const json::exception &e) {
-                                qWarning()
-                                  << "failed to parse room info:" << QString::fromStdString(room)
-                                  << QString::fromStdString(std::string(data.data(), data.size()));
+                                log::db()->warn("failed to parse room info: room_id ({}), {}",
+                                                room,
+                                                std::string(data.data(), data.size()));
                         }
                 } else {
                         // Check if the room is an invite.
@@ -615,10 +629,10 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
                                         room_info.emplace(QString::fromStdString(room),
                                                           std::move(tmp));
                                 } catch (const json::exception &e) {
-                                        qWarning() << "failed to parse room info for invite:"
-                                                   << QString::fromStdString(room)
-                                                   << QString::fromStdString(
-                                                        std::string(data.data(), data.size()));
+                                        log::db()->warn(
+                                          "failed to parse room info for invite: room_id ({}), {}",
+                                          room,
+                                          std::string(data.data(), data.size()));
                                 }
                         }
                 }
@@ -703,7 +717,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
 
                         return QString::fromStdString(msg.content.url);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse m.room.avatar event: {}", e.what());
                 }
         }
 
@@ -726,7 +740,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
                         cursor.close();
                         return QString::fromStdString(m.avatar_url);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse member info: {}", e.what());
                 }
         }
 
@@ -753,7 +767,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
                         if (!msg.content.name.empty())
                                 return QString::fromStdString(msg.content.name);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse m.room.name event: {}", e.what());
                 }
         }
 
@@ -768,7 +782,8 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
                         if (!msg.content.alias.empty())
                                 return QString::fromStdString(msg.content.alias);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse m.room.canonical_alias event: {}",
+                                        e.what());
                 }
         }
 
@@ -784,7 +799,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
                 try {
                         members.emplace(user_id, json::parse(member_data));
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse member info: {}", e.what());
                 }
 
                 ii++;
@@ -828,7 +843,7 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
                           json::parse(std::string(event.data(), event.size()));
                         return msg.content.join_rule;
                 } catch (const json::exception &e) {
-                        qWarning() << e.what();
+                        log::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
                 }
         }
         return JoinRule::Knock;
@@ -850,7 +865,7 @@ Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
                           json::parse(std::string(event.data(), event.size()));
                         return msg.content.guest_access == AccessState::CanJoin;
                 } catch (const json::exception &e) {
-                        qWarning() << e.what();
+                        log::db()->warn("failed to parse m.room.guest_access event: {}", e.what());
                 }
         }
         return false;
@@ -874,7 +889,7 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
                         if (!msg.content.topic.empty())
                                 return QString::fromStdString(msg.content.topic);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse m.room.topic event: {}", e.what());
                 }
         }
 
@@ -897,7 +912,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
                           json::parse(std::string(event.data(), event.size()));
                         return QString::fromStdString(msg.content.name);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse m.room.name event: {}", e.what());
                 }
         }
 
@@ -914,7 +929,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
 
                         return QString::fromStdString(tmp.name);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse member info: {}", e.what());
                 }
         }
 
@@ -939,7 +954,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
                           json::parse(std::string(event.data(), event.size()));
                         return QString::fromStdString(msg.content.url);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse m.room.avatar event: {}", e.what());
                 }
         }
 
@@ -956,7 +971,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
 
                         return QString::fromStdString(tmp.avatar_url);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse member info: {}", e.what());
                 }
         }
 
@@ -981,7 +996,7 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
                           json::parse(std::string(event.data(), event.size()));
                         return QString::fromStdString(msg.content.topic);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        log::db()->warn("failed to parse m.room.topic event: {}", e.what());
                 }
         }
 
@@ -1017,8 +1032,9 @@ Cache::getRoomAvatar(const std::string &room_id)
                         return QImage();
                 }
         } catch (const json::exception &e) {
-                qWarning() << "failed to parse room info" << e.what()
-                           << QString::fromStdString(std::string(response.data(), response.size()));
+                log::db()->warn("failed to parse room info: {}, {}",
+                                e.what(),
+                                std::string(response.data(), response.size()));
         }
 
         if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) {
@@ -1054,7 +1070,7 @@ void
 Cache::populateMembers()
 {
         auto rooms = joinedRooms();
-        qDebug() << "loading" << rooms.size() << "rooms";
+        log::db()->info("loading {} rooms", rooms.size());
 
         auto txn = lmdb::txn::begin(env_);
 
@@ -1182,7 +1198,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_
                                      QString::fromStdString(tmp.name),
                                      QImage::fromData(image(txn, tmp.avatar_url))});
                 } catch (const json::exception &e) {
-                        qWarning() << e.what();
+                        log::db()->warn("{}", e.what());
                 }
 
                 currentIndex += 1;
@@ -1253,7 +1269,7 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
                                   std::min(min_event_level,
                                            (uint16_t)msg.content.state_level(to_string(ty)));
                 } catch (const json::exception &e) {
-                        qWarning() << "hasEnoughPowerLevel: " << e.what();
+                        log::db()->warn("failed to parse m.room.power_levels event: {}", e.what());
                 }
         }
 
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 9ae860fb8..64ce69d62 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -16,13 +16,13 @@
  */
 
 #include <QApplication>
-#include <QDebug>
 #include <QSettings>
 #include <QtConcurrent>
 
 #include "AvatarProvider.h"
 #include "Cache.h"
 #include "ChatPage.h"
+#include "Logging.hpp"
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "OverlayModal.h"
@@ -43,13 +43,12 @@
 #include "dialogs/ReadReceipts.h"
 #include "timeline/TimelineViewManager.h"
 
-constexpr int SYNC_RETRY_TIMEOUT         = 40 * 1000;
-constexpr int INITIAL_SYNC_RETRY_TIMEOUT = 240 * 1000;
-
-ChatPage *ChatPage::instance_ = nullptr;
+ChatPage *ChatPage::instance_             = nullptr;
+constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
 
 ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
   : QWidget(parent)
+  , isConnected_(true)
   , userSettings_{userSettings}
 {
         setObjectName("chatPage");
@@ -78,13 +77,12 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         sidebarActions_   = new SideBarActions(this);
         connect(
           sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage);
-        connect(
-          sidebarActions_, &SideBarActions::joinRoom, http::client(), &MatrixClient::joinRoom);
-        connect(
-          sidebarActions_, &SideBarActions::createRoom, http::client(), &MatrixClient::createRoom);
+        connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom);
+        connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom);
 
         user_info_widget_ = new UserInfoWidget(sideBar_);
         room_list_        = new RoomList(userSettings_, sideBar_);
+        connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom);
 
         sideBarLayout_->addWidget(user_info_widget_);
         sideBarLayout_->addWidget(room_list_);
@@ -107,6 +105,11 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         contentLayout_->addWidget(top_bar_);
         contentLayout_->addWidget(view_manager_);
 
+        connect(this,
+                &ChatPage::removeTimelineEvent,
+                view_manager_,
+                &TimelineViewManager::removeTimelineEvent);
+
         // Splitter
         splitter->addWidget(sideBar_);
         splitter->addWidget(content_);
@@ -120,16 +123,81 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         typingRefresher_ = new QTimer(this);
         typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT);
 
+        connect(this, &ChatPage::connectionLost, this, [this]() {
+                log::net()->info("connectivity lost");
+                isConnected_ = false;
+                http::v2::client()->shutdown();
+                text_input_->disableInput();
+        });
+        connect(this, &ChatPage::connectionRestored, this, [this]() {
+                log::net()->info("trying to re-connect");
+                text_input_->enableInput();
+                isConnected_ = true;
+
+                // Drop all pending connections.
+                http::v2::client()->shutdown();
+                trySync();
+        });
+
+        connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
+        connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
+                if (http::v2::client()->access_token().empty()) {
+                        connectivityTimer_.stop();
+                        return;
+                }
+
+                http::v2::client()->versions(
+                  [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+                          if (err) {
+                                  emit connectionLost();
+                                  return;
+                          }
+
+                          if (!isConnected_)
+                                  emit connectionRestored();
+                  });
+        });
+
+        connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
         connect(user_info_widget_, &UserInfoWidget::logout, this, [this]() {
-                http::client()->logout();
+                http::v2::client()->logout([this](const mtx::responses::Logout &,
+                                                  mtx::http::RequestErr err) {
+                        if (err) {
+                                // TODO: handle special errors
+                                emit contentLoaded();
+                                log::net()->warn("failed to logout: {} - {}",
+                                                 mtx::errors::to_string(err->matrix_error.errcode),
+                                                 err->matrix_error.error);
+                                return;
+                        }
+
+                        emit loggedOut();
+                });
+
                 emit showOverlayProgressBar();
         });
-        connect(http::client(), &MatrixClient::loggedOut, this, &ChatPage::logout);
 
         connect(top_bar_, &TopRoomBar::inviteUsers, this, [this](QStringList users) {
+                const auto room_id = current_room_.toStdString();
+
                 for (int ii = 0; ii < users.size(); ++ii) {
-                        QTimer::singleShot(ii * 1000, this, [this, ii, users]() {
-                                http::client()->inviteUser(current_room_, users.at(ii));
+                        QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() {
+                                const auto user = users.at(ii);
+
+                                http::v2::client()->invite_user(
+                                  room_id,
+                                  user.toStdString(),
+                                  [this, user](const mtx::responses::RoomInvite &,
+                                               mtx::http::RequestErr err) {
+                                          if (err) {
+                                                  emit showNotification(
+                                                    QString("Failed to invite user: %1").arg(user));
+                                                  return;
+                                          }
+
+                                          emit showNotification(
+                                            QString("Invited user: %1").arg(user));
+                                  });
                         });
                 }
         });
@@ -155,36 +223,30 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
 
         connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
                 view_manager_->addRoom(room_id);
-                http::client()->joinRoom(room_id);
+                joinRoom(room_id);
                 room_list_->removeRoom(room_id, currentRoom() == room_id);
         });
 
         connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) {
-                http::client()->leaveRoom(room_id);
+                leaveRoom(room_id);
                 room_list_->removeRoom(room_id, currentRoom() == room_id);
         });
 
-        connect(text_input_, &TextInputWidget::startedTyping, this, [this]() {
-                if (!userSettings_->isTypingNotificationsEnabled())
-                        return;
-
-                typingRefresher_->start();
-                http::client()->sendTypingNotification(current_room_);
-        });
-
+        connect(
+          text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications);
+        connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications);
         connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() {
                 if (!userSettings_->isTypingNotificationsEnabled())
                         return;
 
                 typingRefresher_->stop();
-                http::client()->removeTypingNotification(current_room_);
-        });
-
-        connect(typingRefresher_, &QTimer::timeout, this, [this]() {
-                if (!userSettings_->isTypingNotificationsEnabled())
-                        return;
-
-                http::client()->sendTypingNotification(current_room_);
+                http::v2::client()->stop_typing(
+                  current_room_.toStdString(), [](mtx::http::RequestErr err) {
+                          if (err) {
+                                  log::net()->warn("failed to stop typing notifications: {}",
+                                                   err->matrix_error.error);
+                          }
+                  });
         });
 
         connect(view_manager_,
@@ -207,142 +269,242 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                 view_manager_,
                 SLOT(queueEmoteMessage(const QString &)));
 
-        connect(text_input_,
-                &TextInputWidget::sendJoinRoomRequest,
-                http::client(),
-                &MatrixClient::joinRoom);
+        connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom);
 
         connect(text_input_,
                 &TextInputWidget::uploadImage,
                 this,
-                [this](QSharedPointer<QIODevice> data, const QString &fn) {
-                        http::client()->uploadImage(current_room_, fn, data);
+                [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+                        QMimeDatabase db;
+                        QMimeType mime = db.mimeTypeForData(dev.data());
+
+                        if (!dev->open(QIODevice::ReadOnly)) {
+                                emit uploadFailed(
+                                  QString("Error while reading media: %1").arg(dev->errorString()));
+                                return;
+                        }
+
+                        auto bin     = dev->readAll();
+                        auto payload = std::string(bin.data(), bin.size());
+
+                        http::v2::client()->upload(
+                          payload,
+                          mime.name().toStdString(),
+                          QFileInfo(fn).fileName().toStdString(),
+                          [this,
+                           room_id  = current_room_,
+                           filename = fn,
+                           mime     = mime.name(),
+                           size     = payload.size()](const mtx::responses::ContentURI &res,
+                                                  mtx::http::RequestErr err) {
+                                  if (err) {
+                                          emit uploadFailed(
+                                            tr("Failed to upload image. Please try again."));
+                                          log::net()->warn("failed to upload image: {} ({})",
+                                                           err->matrix_error.error,
+                                                           static_cast<int>(err->status_code));
+                                          return;
+                                  }
+
+                                  emit imageUploaded(room_id,
+                                                     filename,
+                                                     QString::fromStdString(res.content_uri),
+                                                     mime,
+                                                     size);
+                          });
                 });
 
         connect(text_input_,
                 &TextInputWidget::uploadFile,
                 this,
-                [this](QSharedPointer<QIODevice> data, const QString &fn) {
-                        http::client()->uploadFile(current_room_, fn, data);
+                [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+                        QMimeDatabase db;
+                        QMimeType mime = db.mimeTypeForData(dev.data());
+
+                        if (!dev->open(QIODevice::ReadOnly)) {
+                                emit uploadFailed(
+                                  QString("Error while reading media: %1").arg(dev->errorString()));
+                                return;
+                        }
+
+                        auto bin     = dev->readAll();
+                        auto payload = std::string(bin.data(), bin.size());
+
+                        http::v2::client()->upload(
+                          payload,
+                          mime.name().toStdString(),
+                          QFileInfo(fn).fileName().toStdString(),
+                          [this,
+                           room_id  = current_room_,
+                           filename = fn,
+                           mime     = mime.name(),
+                           size     = payload.size()](const mtx::responses::ContentURI &res,
+                                                  mtx::http::RequestErr err) {
+                                  if (err) {
+                                          emit uploadFailed(
+                                            tr("Failed to upload file. Please try again."));
+                                          log::net()->warn("failed to upload file: {} ({})",
+                                                           err->matrix_error.error,
+                                                           static_cast<int>(err->status_code));
+                                          return;
+                                  }
+
+                                  emit fileUploaded(room_id,
+                                                    filename,
+                                                    QString::fromStdString(res.content_uri),
+                                                    mime,
+                                                    size);
+                          });
                 });
 
         connect(text_input_,
                 &TextInputWidget::uploadAudio,
                 this,
-                [this](QSharedPointer<QIODevice> data, const QString &fn) {
-                        http::client()->uploadAudio(current_room_, fn, data);
+                [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+                        QMimeDatabase db;
+                        QMimeType mime = db.mimeTypeForData(dev.data());
+
+                        if (!dev->open(QIODevice::ReadOnly)) {
+                                emit uploadFailed(
+                                  QString("Error while reading media: %1").arg(dev->errorString()));
+                                return;
+                        }
+
+                        auto bin     = dev->readAll();
+                        auto payload = std::string(bin.data(), bin.size());
+
+                        http::v2::client()->upload(
+                          payload,
+                          mime.name().toStdString(),
+                          QFileInfo(fn).fileName().toStdString(),
+                          [this,
+                           room_id  = current_room_,
+                           filename = fn,
+                           mime     = mime.name(),
+                           size     = payload.size()](const mtx::responses::ContentURI &res,
+                                                  mtx::http::RequestErr err) {
+                                  if (err) {
+                                          emit uploadFailed(
+                                            tr("Failed to upload audio. Please try again."));
+                                          log::net()->warn("failed to upload audio: {} ({})",
+                                                           err->matrix_error.error,
+                                                           static_cast<int>(err->status_code));
+                                          return;
+                                  }
+
+                                  emit audioUploaded(room_id,
+                                                     filename,
+                                                     QString::fromStdString(res.content_uri),
+                                                     mime,
+                                                     size);
+                          });
                 });
         connect(text_input_,
                 &TextInputWidget::uploadVideo,
                 this,
-                [this](QSharedPointer<QIODevice> data, const QString &fn) {
-                        http::client()->uploadVideo(current_room_, fn, data);
+                [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+                        QMimeDatabase db;
+                        QMimeType mime = db.mimeTypeForData(dev.data());
+
+                        if (!dev->open(QIODevice::ReadOnly)) {
+                                emit uploadFailed(
+                                  QString("Error while reading media: %1").arg(dev->errorString()));
+                                return;
+                        }
+
+                        auto bin     = dev->readAll();
+                        auto payload = std::string(bin.data(), bin.size());
+
+                        http::v2::client()->upload(
+                          payload,
+                          mime.name().toStdString(),
+                          QFileInfo(fn).fileName().toStdString(),
+                          [this,
+                           room_id  = current_room_,
+                           filename = fn,
+                           mime     = mime.name(),
+                           size     = payload.size()](const mtx::responses::ContentURI &res,
+                                                  mtx::http::RequestErr err) {
+                                  if (err) {
+                                          emit uploadFailed(
+                                            tr("Failed to upload video. Please try again."));
+                                          log::net()->warn("failed to upload video: {} ({})",
+                                                           err->matrix_error.error,
+                                                           static_cast<int>(err->status_code));
+                                          return;
+                                  }
+
+                                  emit videoUploaded(room_id,
+                                                     filename,
+                                                     QString::fromStdString(res.content_uri),
+                                                     mime,
+                                                     size);
+                          });
                 });
 
-        connect(
-          http::client(), &MatrixClient::roomCreationFailed, this, &ChatPage::showNotification);
-        connect(http::client(), &MatrixClient::joinFailed, this, &ChatPage::showNotification);
-        connect(http::client(), &MatrixClient::uploadFailed, this, [this](int, const QString &msg) {
+        connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) {
                 text_input_->hideUploadSpinner();
                 emit showNotification(msg);
         });
-        connect(
-          http::client(),
-          &MatrixClient::imageUploaded,
-          this,
-          [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
-                  text_input_->hideUploadSpinner();
-                  view_manager_->queueImageMessage(roomid, filename, url, mime, dsize);
-          });
-        connect(
-          http::client(),
-          &MatrixClient::fileUploaded,
-          this,
-          [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
-                  text_input_->hideUploadSpinner();
-                  view_manager_->queueFileMessage(roomid, filename, url, mime, dsize);
-          });
-        connect(
-          http::client(),
-          &MatrixClient::audioUploaded,
-          this,
-          [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
-                  text_input_->hideUploadSpinner();
-                  view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize);
-          });
-        connect(
-          http::client(),
-          &MatrixClient::videoUploaded,
-          this,
-          [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) {
-                  text_input_->hideUploadSpinner();
-                  view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize);
-          });
-
-        connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
-
-        connect(http::client(),
-                &MatrixClient::initialSyncCompleted,
-                this,
-                &ChatPage::initialSyncCompleted);
-        connect(
-          http::client(), &MatrixClient::initialSyncFailed, this, &ChatPage::retryInitialSync);
-        connect(http::client(), &MatrixClient::syncCompleted, this, &ChatPage::syncCompleted);
-        connect(http::client(),
-                &MatrixClient::getOwnProfileResponse,
+        connect(this,
+                &ChatPage::imageUploaded,
                 this,
-                &ChatPage::updateOwnProfileInfo);
-        connect(http::client(),
-                SIGNAL(getOwnCommunitiesResponse(QList<QString>)),
+                [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+                        text_input_->hideUploadSpinner();
+                        view_manager_->queueImageMessage(roomid, filename, url, mime, dsize);
+                });
+        connect(this,
+                &ChatPage::fileUploaded,
                 this,
-                SLOT(updateOwnCommunitiesInfo(QList<QString>)));
-        connect(http::client(),
-                &MatrixClient::communityProfileRetrieved,
+                [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+                        text_input_->hideUploadSpinner();
+                        view_manager_->queueFileMessage(roomid, filename, url, mime, dsize);
+                });
+        connect(this,
+                &ChatPage::audioUploaded,
                 this,
-                [this](QString communityId, QJsonObject profile) {
-                        communities_[communityId]->parseProfile(profile);
+                [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+                        text_input_->hideUploadSpinner();
+                        view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize);
                 });
-        connect(http::client(),
-                &MatrixClient::communityRoomsRetrieved,
+        connect(this,
+                &ChatPage::videoUploaded,
                 this,
-                [this](QString communityId, QJsonObject rooms) {
-                        communities_[communityId]->parseRooms(rooms);
-
-                        if (communityId == current_community_) {
-                                if (communityId == "world") {
-                                        room_list_->setFilterRooms(false);
-                                } else {
-                                        room_list_->setRoomFilter(
-                                          communities_[communityId]->getRoomList());
-                                }
-                        }
+                [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
+                        text_input_->hideUploadSpinner();
+                        view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize);
                 });
 
-        connect(http::client(), &MatrixClient::joinedRoom, this, [this](const QString &room_id) {
-                emit showNotification("You joined the room.");
+        connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
 
-                // We remove any invites with the same room_id.
-                try {
-                        cache::client()->removeInvite(room_id.toStdString());
-                } catch (const lmdb::error &e) {
-                        emit showNotification(QString("Failed to remove invite: %1")
-                                                .arg(QString::fromStdString(e.what())));
-                }
-        });
-        connect(http::client(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom);
-        connect(http::client(), &MatrixClient::invitedUser, this, [this](QString, QString user) {
-                emit showNotification(QString("Invited user %1").arg(user));
-        });
-        connect(http::client(), &MatrixClient::roomCreated, this, [this](QString room_id) {
-                emit showNotification(QString("Room %1 created").arg(room_id));
-        });
-        connect(http::client(), &MatrixClient::redactionFailed, this, [this](const QString &error) {
-                emit showNotification(QString("Message redaction failed: %1").arg(error));
-        });
-        connect(http::client(),
-                &MatrixClient::notificationsRetrieved,
-                this,
-                &ChatPage::sendDesktopNotifications);
+        // connect(http::client(),
+        //         SIGNAL(getOwnCommunitiesResponse(QList<QString>)),
+        //         this,
+        //         SLOT(updateOwnCommunitiesInfo(QList<QString>)));
+        // connect(http::client(),
+        //         &MatrixClient::communityProfileRetrieved,
+        //         this,
+        //         [this](QString communityId, QJsonObject profile) {
+        //                 communities_[communityId]->parseProfile(profile);
+        //         });
+        // connect(http::client(),
+        //         &MatrixClient::communityRoomsRetrieved,
+        //         this,
+        //         [this](QString communityId, QJsonObject rooms) {
+        //                 communities_[communityId]->parseRooms(rooms);
+
+        //                 if (communityId == current_community_) {
+        //                         if (communityId == "world") {
+        //                                 room_list_->setFilterRooms(false);
+        //                         } else {
+        //                                 room_list_->setRoomFilter(
+        //                                   communities_[communityId]->getRoomList());
+        //                         }
+        //                 }
+        //         });
+
+        connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
+        connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications);
 
         showContentTimer_ = new QTimer(this);
         showContentTimer_->setSingleShot(true);
@@ -361,20 +523,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                 }
         });
 
-        initialSyncTimer_ = new QTimer(this);
-        connect(initialSyncTimer_, &QTimer::timeout, this, [this]() { retryInitialSync(); });
-
-        syncTimeoutTimer_ = new QTimer(this);
-        connect(syncTimeoutTimer_, &QTimer::timeout, this, [this]() {
-                if (http::client()->getHomeServer().isEmpty()) {
-                        syncTimeoutTimer_->stop();
-                        return;
-                }
-
-                qDebug() << "Sync took too long. Retrying...";
-                http::client()->sync();
-        });
-
         connect(communitiesList_,
                 &CommunitiesList::communityChanged,
                 this,
@@ -394,12 +542,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                 this,
                 &ChatPage::setGroupViewState);
 
-        connect(this, &ChatPage::continueSync, this, [this](const QString &next_batch) {
-                syncTimeoutTimer_->start(SYNC_RETRY_TIMEOUT);
-                http::client()->setNextBatchToken(next_batch);
-                http::client()->sync();
-        });
-
         connect(this, &ChatPage::startConsesusTimer, this, [this]() {
                 consensusTimer_->start(CONSENSUS_TIMEOUT);
                 showContentTimer_->start(SHOW_CONTENT_TIMEOUT);
@@ -418,7 +560,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                 try {
                         room_list_->cleanupInvites(cache::client()->invites());
                 } catch (const lmdb::error &e) {
-                        qWarning() << "failed to retrieve invites" << e.what();
+                        log::db()->error("failed to retrieve invites: {}", e.what());
                 }
 
                 view_manager_->initialize(rooms);
@@ -437,7 +579,20 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                 }
 
                 if (hasNotifications)
-                        http::client()->getNotifications();
+                        http::v2::client()->notifications(
+                          5,
+                          [this](const mtx::responses::Notifications &res,
+                                 mtx::http::RequestErr err) {
+                                  if (err) {
+                                          log::net()->warn(
+                                            "failed to retrieve notifications: {} ({})",
+                                            err->matrix_error.error,
+                                            static_cast<int>(err->status_code));
+                                          return;
+                                  }
+
+                                  emit notificationsRetrieved(std::move(res));
+                          });
         });
         connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
         connect(
@@ -446,12 +601,21 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                           changeTopRoomInfo(currentRoom());
           });
 
-        instance_ = this;
+        // Callbacks to update the user info (top left corner of the page).
+        connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar);
+        connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) {
+                QSettings settings;
+                auto userid = settings.value("auth/user_id").toString();
+                user_info_widget_->setUserId(userid);
+                user_info_widget_->setDisplayName(name);
+        });
+
+        connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync);
+        connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync);
 
-        qRegisterMetaType<std::map<QString, RoomInfo>>();
-        qRegisterMetaType<QMap<QString, RoomInfo>>();
-        qRegisterMetaType<mtx::responses::Rooms>();
-        qRegisterMetaType<std::vector<std::string>>();
+        connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
+
+        instance_ = this;
 }
 
 void
@@ -462,6 +626,19 @@ ChatPage::logout()
         resetUI();
 
         emit closing();
+        connectivityTimer_.stop();
+}
+
+void
+ChatPage::dropToLoginPage(const QString &msg)
+{
+        deleteConfigs();
+        resetUI();
+
+        http::v2::client()->shutdown();
+        connectivityTimer_.stop();
+
+        emit showLoginPage(msg);
 }
 
 void
@@ -490,17 +667,66 @@ ChatPage::deleteConfigs()
         settings.endGroup();
 
         cache::client()->deleteData();
-
-        http::client()->reset();
+        http::v2::client()->clear();
 }
 
 void
 ChatPage::bootstrap(QString userid, QString homeserver, QString token)
 {
-        http::client()->setServer(homeserver);
-        http::client()->setAccessToken(token);
-        http::client()->getOwnProfile();
-        http::client()->getOwnCommunities();
+        using namespace mtx::identifiers;
+
+        try {
+                http::v2::client()->set_user(parse<User>(userid.toStdString()));
+        } catch (const std::invalid_argument &e) {
+                log::main()->critical("bootstrapped with invalid user_id: {}",
+                                      userid.toStdString());
+        }
+
+        http::v2::client()->set_server(homeserver.toStdString());
+        http::v2::client()->set_access_token(token.toStdString());
+        http::v2::client()->get_profile(
+          userid.toStdString(),
+          [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          log::net()->warn("failed to retrieve own profile info");
+                          return;
+                  }
+
+                  emit setUserDisplayName(QString::fromStdString(res.display_name));
+
+                  if (cache::client()) {
+                          auto data = cache::client()->image(res.avatar_url);
+                          if (!data.isNull()) {
+                                  emit setUserAvatar(QImage::fromData(data));
+                                  return;
+                          }
+                  }
+
+                  if (res.avatar_url.empty())
+                          return;
+
+                  http::v2::client()->download(
+                    res.avatar_url,
+                    [this, res](const std::string &data,
+                                const std::string &,
+                                const std::string &,
+                                mtx::http::RequestErr err) {
+                            if (err) {
+                                    log::net()->warn(
+                                      "failed to download user avatar: {} - {}",
+                                      mtx::errors::to_string(err->matrix_error.errcode),
+                                      err->matrix_error.error);
+                                    return;
+                            }
+
+                            if (cache::client())
+                                    cache::client()->saveImage(res.avatar_url, data);
+
+                            emit setUserAvatar(
+                              QImage::fromData(QByteArray(data.data(), data.size())));
+                    });
+          });
+        // TODO http::client()->getOwnCommunities();
 
         cache::init(userid);
 
@@ -518,62 +744,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
                         return;
                 }
         } catch (const lmdb::error &e) {
-                qCritical() << "Cache failure" << e.what();
+                log::db()->critical("failure during boot: {}", e.what());
                 cache::client()->deleteData();
-                qInfo() << "Falling back to initial sync ...";
+                log::net()->info("falling back to initial sync");
         }
 
-        http::client()->initialSync();
-
-        initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT);
-}
-
-void
-ChatPage::syncCompleted(const mtx::responses::Sync &response)
-{
-        syncTimeoutTimer_->stop();
-
-        QtConcurrent::run([this, res = std::move(response)]() {
-                try {
-                        cache::client()->saveState(res);
-                        emit syncUI(res.rooms);
-
-                        auto updates = cache::client()->roomUpdates(res);
-
-                        emit syncTopBar(updates);
-                        emit syncRoomlist(updates);
-
-                } catch (const lmdb::error &e) {
-                        std::cout << "save cache error:" << e.what() << '\n';
-                        // TODO: retry sync.
-                        return;
-                }
-
-                emit continueSync(cache::client()->nextBatchToken());
-        });
-}
-
-void
-ChatPage::initialSyncCompleted(const mtx::responses::Sync &response)
-{
-        initialSyncTimer_->stop();
-
-        qDebug() << "initial sync completed";
-
-        QtConcurrent::run([this, res = std::move(response)]() {
-                try {
-                        cache::client()->saveState(res);
-                        emit initializeViews(std::move(res.rooms));
-                        emit initializeRoomList(cache::client()->roomInfo());
-                } catch (const lmdb::error &e) {
-                        qWarning() << "cache error:" << QString::fromStdString(e.what());
-                        emit retryInitialSync();
-                        return;
-                }
-
-                emit continueSync(cache::client()->nextBatchToken());
-                emit contentLoaded();
-        });
+        tryInitialSync();
 }
 
 void
@@ -585,41 +761,6 @@ ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img)
         top_bar_->updateRoomAvatar(img.toImage());
 }
 
-void
-ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name)
-{
-        QSettings settings;
-        auto userid = settings.value("auth/user_id").toString();
-
-        user_info_widget_->setUserId(userid);
-        user_info_widget_->setDisplayName(display_name);
-
-        if (!avatar_url.isValid())
-                return;
-
-        if (cache::client()) {
-                auto data = cache::client()->image(avatar_url.toString());
-                if (!data.isNull()) {
-                        user_info_widget_->setAvatar(QImage::fromData(data));
-                        return;
-                }
-        }
-
-        auto proxy = http::client()->fetchUserAvatar(avatar_url);
-
-        if (proxy.isNull())
-                return;
-
-        proxy->setParent(this);
-        connect(proxy.data(),
-                &DownloadMediaProxy::avatarDownloaded,
-                this,
-                [this, proxy](const QImage &img) {
-                        proxy->deleteLater();
-                        user_info_widget_->setAvatar(img);
-                });
-}
-
 void
 ChatPage::updateOwnCommunitiesInfo(const QList<QString> &own_communities)
 {
@@ -636,7 +777,7 @@ void
 ChatPage::changeTopRoomInfo(const QString &room_id)
 {
         if (room_id.isEmpty()) {
-                qWarning() << "can't switch to empty room_id";
+                log::main()->warn("cannot switch to empty room_id");
                 return;
         }
 
@@ -660,7 +801,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id)
                         top_bar_->updateRoomAvatar(img);
 
         } catch (const lmdb::error &e) {
-                qWarning() << "failed to change top bar room info" << e.what();
+                log::main()->error("failed to change top bar room info: {}", e.what());
         }
 
         current_room_ = room_id;
@@ -681,7 +822,7 @@ ChatPage::showUnreadMessageNotification(int count)
 void
 ChatPage::loadStateFromCache()
 {
-        qDebug() << "restoring state from cache";
+        log::db()->info("restoring state from cache");
 
         QtConcurrent::run([this]() {
                 try {
@@ -696,7 +837,7 @@ ChatPage::loadStateFromCache()
                 }
 
                 // Start receiving events.
-                emit continueSync(cache::client()->nextBatchToken());
+                emit trySyncCb();
 
                 // Check periodically if the timelines have been loaded.
                 emit startConsesusTimer();
@@ -740,7 +881,7 @@ ChatPage::removeRoom(const QString &room_id)
                 cache::client()->removeRoom(room_id);
                 cache::client()->removeInvite(room_id.toStdString());
         } catch (const lmdb::error &e) {
-                qCritical() << "The cache couldn't be updated: " << e.what();
+                log::db()->critical("failure while removing room: {}", e.what());
                 // TODO: Notify the user.
         }
 
@@ -823,33 +964,6 @@ ChatPage::setGroupViewState(bool isEnabled)
         communitiesList_->show();
 }
 
-void
-ChatPage::retryInitialSync(int status_code)
-{
-        initialSyncTimer_->stop();
-
-        if (http::client()->getHomeServer().isEmpty()) {
-                deleteConfigs();
-                resetUI();
-                emit showLoginPage("Sync error. Please try again.");
-                return;
-        }
-
-        // Retry on Bad-Gateway & Gateway-Timeout errors
-        if (status_code == -1 || status_code == 504 || status_code == 502 || status_code == 524) {
-                qWarning() << "retrying initial sync";
-
-                http::client()->initialSync();
-                initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT);
-        } else {
-                // Drop into the login screen.
-                deleteConfigs();
-                resetUI();
-
-                emit showLoginPage(QString("Sync error %1. Please try again.").arg(status_code));
-        }
-}
-
 void
 ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count)
 {
@@ -886,7 +1000,197 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res)
                                   utils::event_body(item.event));
                         }
                 } catch (const lmdb::error &e) {
-                        qWarning() << e.what();
+                        log::db()->warn("error while sending desktop notification: {}", e.what());
                 }
         }
 }
+
+void
+ChatPage::tryInitialSync()
+{
+        mtx::http::SyncOpts opts;
+        opts.timeout = 0;
+
+        log::net()->info("trying initial sync");
+
+        http::v2::client()->sync(
+          opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          const auto error      = QString::fromStdString(err->matrix_error.error);
+                          const auto msg        = tr("Please try to login again: %1").arg(error);
+                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+                          const int status_code = static_cast<int>(err->status_code);
+
+                          log::net()->error("sync error: {} {}", status_code, err_code);
+
+                          switch (status_code) {
+                          case 502:
+                          case 504:
+                          case 524: {
+                                  emit tryInitialSyncCb();
+                                  return;
+                          }
+                          default: {
+                                  emit dropToLoginPageCb(msg);
+                                  return;
+                          }
+                          }
+                  }
+
+                  log::net()->info("initial sync completed");
+
+                  try {
+                          cache::client()->saveState(res);
+                          emit initializeViews(std::move(res.rooms));
+                          emit initializeRoomList(cache::client()->roomInfo());
+                  } catch (const lmdb::error &e) {
+                          log::db()->error("{}", e.what());
+                          emit tryInitialSyncCb();
+                          return;
+                  }
+
+                  emit trySyncCb();
+                  emit contentLoaded();
+          });
+}
+
+void
+ChatPage::trySync()
+{
+        mtx::http::SyncOpts opts;
+
+        if (!connectivityTimer_.isActive())
+                connectivityTimer_.start();
+
+        try {
+                opts.since = cache::client()->nextBatchToken();
+        } catch (const lmdb::error &e) {
+                log::db()->error("failed to retrieve next batch token: {}", e.what());
+                return;
+        }
+
+        http::v2::client()->sync(
+          opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          const auto error      = QString::fromStdString(err->matrix_error.error);
+                          const auto msg        = tr("Please try to login again: %1").arg(error);
+                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+                          const int status_code = static_cast<int>(err->status_code);
+
+                          log::net()->error("sync error: {} {}", status_code, err_code);
+
+                          switch (status_code) {
+                          case 502:
+                          case 504:
+                          case 524: {
+                                  emit trySync();
+                                  return;
+                          }
+                          case 401:
+                          case 403: {
+                                  // We are logged out.
+                                  if (http::v2::client()->access_token().empty())
+                                          return;
+
+                                  emit dropToLoginPageCb(msg);
+                                  return;
+                          }
+                          default: {
+                                  emit trySync();
+                                  return;
+                          }
+                          }
+                  }
+
+                  log::net()->debug("sync completed: {}", res.next_batch);
+
+                  // TODO: fine grained error handling
+                  try {
+                          cache::client()->saveState(res);
+                          emit syncUI(res.rooms);
+
+                          auto updates = cache::client()->roomUpdates(res);
+
+                          emit syncTopBar(updates);
+                          emit syncRoomlist(updates);
+                  } catch (const lmdb::error &e) {
+                          log::db()->error("saving sync response: {}", e.what());
+                  }
+
+                  emit trySyncCb();
+          });
+}
+
+void
+ChatPage::joinRoom(const QString &room)
+{
+        const auto room_id = room.toStdString();
+
+        http::v2::client()->join_room(
+          room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) {
+                  if (err) {
+                          emit showNotification(
+                            QString("Failed to join room: %1")
+                              .arg(QString::fromStdString(err->matrix_error.error)));
+                          return;
+                  }
+
+                  emit showNotification("You joined the room");
+
+                  // We remove any invites with the same room_id.
+                  try {
+                          cache::client()->removeInvite(room_id);
+                  } catch (const lmdb::error &e) {
+                          emit showNotification(
+                            QString("Failed to remove invite: %1").arg(e.what()));
+                  }
+          });
+}
+
+void
+ChatPage::createRoom(const mtx::requests::CreateRoom &req)
+{
+        http::v2::client()->create_room(
+          req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          emit showNotification(
+                            tr("Room creation failed: %1")
+                              .arg(QString::fromStdString(err->matrix_error.error)));
+                          return;
+                  }
+
+                  emit showNotification(QString("Room %1 created")
+                                          .arg(QString::fromStdString(res.room_id.to_string())));
+          });
+}
+
+void
+ChatPage::leaveRoom(const QString &room_id)
+{
+        http::v2::client()->leave_room(
+          room_id.toStdString(), [this, room_id](const json &, mtx::http::RequestErr err) {
+                  if (err) {
+                          emit showNotification(
+                            tr("Failed to leave room: %1")
+                              .arg(QString::fromStdString(err->matrix_error.error)));
+                          return;
+                  }
+
+                  emit leftRoom(room_id);
+          });
+}
+
+void
+ChatPage::sendTypingNotifications()
+{
+        if (!userSettings_->isTypingNotificationsEnabled())
+                return;
+
+        http::v2::client()->start_typing(
+          current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) {
+                  if (err) {
+                          log::net()->warn("failed to send typing notification: {}",
+                                           err->matrix_error.error);
+                  }
+          });
+}
diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc
index 0d7f5aab0..8ccd5e9d5 100644
--- a/src/CommunitiesList.cc
+++ b/src/CommunitiesList.cc
@@ -1,4 +1,6 @@
+#include "Cache.h"
 #include "CommunitiesList.h"
+#include "Logging.hpp"
 #include "MatrixClient.h"
 
 #include <QLabel>
@@ -38,17 +40,14 @@ CommunitiesList::CommunitiesList(QWidget *parent)
         scrollArea_->setWidget(scrollAreaContents_);
         topLayout_->addWidget(scrollArea_);
 
-        connect(http::client(),
-                &MatrixClient::communityProfileRetrieved,
-                this,
-                [](QString communityId, QJsonObject profile) {
-                        http::client()->fetchCommunityAvatar(
-                          communityId, QUrl(profile["avatar_url"].toString()));
-                });
-        connect(http::client(),
-                SIGNAL(communityAvatarRetrieved(const QString &, const QPixmap &)),
-                this,
-                SLOT(updateCommunityAvatar(const QString &, const QPixmap &)));
+        // connect(http::client(),
+        //         &MatrixClient::communityProfileRetrieved,
+        //         this,
+        //         [this](QString communityId, QJsonObject profile) {
+        //                 fetchCommunityAvatar(communityId, profile["avatar_url"].toString());
+        //         });
+        connect(
+          this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar);
 }
 
 void
@@ -61,8 +60,8 @@ CommunitiesList::setCommunities(const std::map<QString, QSharedPointer<Community
         for (const auto &community : communities) {
                 addCommunity(community.second, community.first);
 
-                http::client()->fetchCommunityProfile(community.first);
-                http::client()->fetchCommunityRooms(community.first);
+                // http::client()->fetchCommunityProfile(community.first);
+                // http::client()->fetchCommunityRooms(community.first);
         }
 
         communities_["world"]->setPressedState(true);
@@ -77,7 +76,7 @@ CommunitiesList::addCommunity(QSharedPointer<Community> community, const QString
 
         communities_.emplace(community_id, QSharedPointer<CommunitiesListItem>(list_item));
 
-        http::client()->fetchCommunityAvatar(community_id, community->getAvatar());
+        fetchCommunityAvatar(community_id, community->getAvatar().toString());
 
         contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item);
 
@@ -117,3 +116,37 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id)
                 }
         }
 }
+
+void
+CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
+{
+        auto savedImgData = cache::client()->image(avatarUrl);
+        if (!savedImgData.isNull()) {
+                QPixmap pix;
+                pix.loadFromData(savedImgData);
+                emit avatarRetrieved(id, pix);
+                return;
+        }
+
+        mtx::http::ThumbOpts opts;
+        opts.mxc_url = avatarUrl.toStdString();
+        http::v2::client()->get_thumbnail(
+          opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          log::net()->warn("failed to download avatar: {} - ({} {})",
+                                           opts.mxc_url,
+                                           mtx::errors::to_string(err->matrix_error.errcode),
+                                           err->matrix_error.error);
+                          return;
+                  }
+
+                  cache::client()->saveImage(opts.mxc_url, res);
+
+                  auto data = QByteArray(res.data(), res.size());
+
+                  QPixmap pix;
+                  pix.loadFromData(data);
+
+                  emit avatarRetrieved(id, pix);
+          });
+}
diff --git a/src/Logging.cpp b/src/Logging.cpp
new file mode 100644
index 000000000..c6c1c502d
--- /dev/null
+++ b/src/Logging.cpp
@@ -0,0 +1,50 @@
+#include "Logging.hpp"
+
+#include <iostream>
+#include <spdlog/sinks/file_sinks.h>
+
+namespace {
+std::shared_ptr<spdlog::logger> db_logger   = nullptr;
+std::shared_ptr<spdlog::logger> net_logger  = nullptr;
+std::shared_ptr<spdlog::logger> main_logger = nullptr;
+
+constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
+constexpr auto MAX_LOG_FILES = 3;
+}
+
+namespace log {
+void
+init(const std::string &file_path)
+{
+        auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
+          file_path, MAX_FILE_SIZE, MAX_LOG_FILES);
+
+        auto console_sink = std::make_shared<spdlog::sinks::stdout_sink_mt>();
+
+        std::vector<spdlog::sink_ptr> sinks;
+        sinks.push_back(file_sink);
+        sinks.push_back(console_sink);
+
+        net_logger  = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks));
+        main_logger = std::make_shared<spdlog::logger>("main", std::begin(sinks), std::end(sinks));
+        db_logger   = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
+}
+
+std::shared_ptr<spdlog::logger>
+main()
+{
+        return main_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+net()
+{
+        return net_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+db()
+{
+        return db_logger;
+}
+}
diff --git a/src/LoginPage.cc b/src/LoginPage.cc
index c7f9b042d..d695a7593 100644
--- a/src/LoginPage.cc
+++ b/src/LoginPage.cc
@@ -137,16 +137,16 @@ LoginPage::LoginPage(QWidget *parent)
 
         setLayout(top_layout_);
 
+        connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk);
+        connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError);
+        connect(this, &LoginPage::loginErrorCb, this, &LoginPage::loginError);
+
         connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
         connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
         connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
         connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
         connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-        connect(http::client(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString)));
-        connect(http::client(), SIGNAL(loginError(QString)), this, SIGNAL(errorOccurred()));
         connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered()));
-        connect(http::client(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString)));
-        connect(http::client(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess()));
         connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
 }
 
@@ -180,17 +180,47 @@ LoginPage::onMatrixIdEntered()
 
                 inferredServerAddress_ = homeServer;
                 serverInput_->setText(homeServer);
-                http::client()->setServer(homeServer);
-                http::client()->versions();
+
+                http::v2::client()->set_server(user.hostname());
+                checkHomeserverVersion();
         }
 }
 
+void
+LoginPage::checkHomeserverVersion()
+{
+        http::v2::client()->versions(
+          [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+                  if (err) {
+                          using namespace boost::beast::http;
+
+                          if (err->status_code == status::not_found) {
+                                  emit versionErrorCb(tr("The required endpoints were not found. "
+                                                         "Possibly not a Matrix server."));
+                                  return;
+                          }
+
+                          if (!err->parse_error.empty()) {
+                                  emit versionErrorCb(tr("Received malformed response. Make sure "
+                                                         "the homeserver domain is valid."));
+                                  return;
+                          }
+
+                          emit versionErrorCb(tr(
+                            "An unknown error occured. Make sure the homeserver domain is valid."));
+                          return;
+                  }
+
+                  emit versionOkCb();
+          });
+}
+
 void
 LoginPage::onServerAddressEntered()
 {
         error_label_->setText("");
-        http::client()->setServer(serverInput_->text());
-        http::client()->versions();
+        http::v2::client()->set_server(serverInput_->text().toStdString());
+        checkHomeserverVersion();
 
         serverLayout_->removeWidget(errorIcon_);
         errorIcon_->hide();
@@ -199,11 +229,8 @@ LoginPage::onServerAddressEntered()
 }
 
 void
-LoginPage::versionError(QString error)
+LoginPage::versionError(const QString &error)
 {
-        QUrl currentServer  = http::client()->getHomeServer();
-        QString mxidAddress = matrixid_input_->text().split(":").at(1);
-
         error_label_->setText(error);
         serverInput_->show();
 
@@ -215,7 +242,7 @@ LoginPage::versionError(QString error)
 }
 
 void
-LoginPage::versionSuccess()
+LoginPage::versionOk()
 {
         serverLayout_->removeWidget(spinner_);
         matrixidLayout_->removeWidget(spinner_);
@@ -241,8 +268,20 @@ LoginPage::onLoginButtonClicked()
         if (password_input_->text().isEmpty())
                 return loginError(tr("Empty password"));
 
-        http::client()->setServer(serverInput_->text());
-        http::client()->login(QString::fromStdString(user.localpart()), password_input_->text());
+        http::v2::client()->set_server(serverInput_->text().toStdString());
+        http::v2::client()->login(
+          user.localpart(),
+          password_input_->text().toStdString(),
+          initialDeviceName(),
+          [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          emit loginError(QString::fromStdString(err->matrix_error.error));
+                          emit errorOccurred();
+                          return;
+                  }
+
+                  emit loginOk(res);
+          });
 
         emit loggingIn();
 }
diff --git a/src/MainWindow.cc b/src/MainWindow.cc
index c46cbff17..9ba8b28e4 100644
--- a/src/MainWindow.cc
+++ b/src/MainWindow.cc
@@ -17,7 +17,6 @@
 
 #include <QApplication>
 #include <QLayout>
-#include <QNetworkReply>
 #include <QSettings>
 #include <QShortcut>
 
@@ -26,6 +25,7 @@
 #include "ChatPage.h"
 #include "Config.h"
 #include "LoadingIndicator.h"
+#include "Logging.hpp"
 #include "LoginPage.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
@@ -46,6 +46,15 @@
 
 MainWindow *MainWindow::instance_ = nullptr;
 
+MainWindow::~MainWindow()
+{
+        if (http::v2::client() != nullptr) {
+                http::v2::client()->shutdown();
+                // TODO: find out why waiting for the threads to join is slow.
+                http::v2::client()->close();
+        }
+}
+
 MainWindow::MainWindow(QWidget *parent)
   : QMainWindow(parent)
   , progressModal_{nullptr}
@@ -54,9 +63,6 @@ MainWindow::MainWindow(QWidget *parent)
         setWindowTitle("nheko");
         setObjectName("MainWindow");
 
-        // Initialize the http client.
-        http::init();
-
         restoreWindowSize();
 
         QFont font("Open Sans");
@@ -124,21 +130,13 @@ MainWindow::MainWindow(QWidget *parent)
         connect(
           chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
 
-        connect(http::client(),
-                SIGNAL(loginSuccess(QString, QString, QString)),
-                this,
-                SLOT(showChatPage(QString, QString, QString)));
-
-        connect(http::client(),
-                SIGNAL(registerSuccess(QString, QString, QString)),
-                this,
-                SLOT(showChatPage(QString, QString, QString)));
-        connect(http::client(), &MatrixClient::invalidToken, this, [this]() {
-                chat_page_->deleteConfigs();
-                showLoginPage();
-                login_page_->loginError("Invalid token detected. Please try to login again.");
+        connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
+                http::v2::client()->set_user(res.user_id);
+                showChatPage();
         });
 
+        connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
+
         QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
         connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
 
@@ -157,7 +155,18 @@ MainWindow::MainWindow(QWidget *parent)
                 QString home_server = settings.value("auth/home_server").toString();
                 QString user_id     = settings.value("auth/user_id").toString();
 
-                showChatPage(user_id, home_server, token);
+                http::v2::client()->set_access_token(token.toStdString());
+                http::v2::client()->set_server(home_server.toStdString());
+
+                try {
+                        using namespace mtx::identifiers;
+                        http::v2::client()->set_user(parse<User>(user_id.toStdString()));
+                } catch (const std::invalid_argument &e) {
+                        log::main()->critical("bootstrapped with invalid user_id: {}",
+                                              user_id.toStdString());
+                }
+
+                showChatPage();
         }
 }
 
@@ -216,8 +225,13 @@ MainWindow::removeOverlayProgressBar()
 }
 
 void
-MainWindow::showChatPage(QString userid, QString homeserver, QString token)
+MainWindow::showChatPage()
 {
+        auto userid     = QString::fromStdString(http::v2::client()->user_id().to_string());
+        auto homeserver = QString::fromStdString(http::v2::client()->server() + ":" +
+                                                 std::to_string(http::v2::client()->port()));
+        auto token      = QString::fromStdString(http::v2::client()->access_token());
+
         QSettings settings;
         settings.setValue("auth/access_token", token);
         settings.setValue("auth/home_server", homeserver);
@@ -317,7 +331,7 @@ MainWindow::openLeaveRoomDialog(const QString &room_id)
                         leaveRoomModal_->hide();
 
                         if (leaving)
-                                http::client()->leaveRoom(roomToLeave);
+                                chat_page_->leaveRoom(roomToLeave);
                 });
 
         leaveRoomModal_ =
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index c4eaf3471..0eb4658ab 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -1,1371 +1,32 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QDebug>
-#include <QFile>
-#include <QImageReader>
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QMimeDatabase>
-#include <QNetworkReply>
-#include <QNetworkRequest>
-#include <QPixmap>
-#include <QProcessEnvironment>
-#include <QSettings>
-#include <QUrlQuery>
-#include <QtConcurrent>
-#include <mtx/errors.hpp>
-
 #include "MatrixClient.h"
-#include <mtxclient/http/client.hpp>
+
+#include <memory>
 
 namespace {
-std::unique_ptr<MatrixClient> instance_ = nullptr;
+auto v2_client_ = std::make_shared<mtx::http::Client>("matrix.org");
 }
 
 namespace http {
+namespace v2 {
 
-std::shared_ptr<mtx::http::Client> client_ = nullptr;
-
-void
-init()
-{
-        if (!instance_)
-                instance_ = std::make_unique<MatrixClient>();
-}
-
-MatrixClient *
+mtx::http::Client *
 client()
 {
-        return instance_.get();
-}
-}
-
-MatrixClient::MatrixClient(QObject *parent)
-  : QNetworkAccessManager(parent)
-  , clientApiUrl_{"/_matrix/client/r0"}
-  , mediaApiUrl_{"/_matrix/media/r0"}
-  , serverProtocol_{"https"}
-{
-        qRegisterMetaType<mtx::responses::Sync>();
-
-        QSettings settings;
-        txn_id_ = settings.value("client/transaction_id", 1).toInt();
-
-        auto env = QProcessEnvironment::systemEnvironment();
-
-        auto allowInsecureConnections = env.value("NHEKO_ALLOW_INSECURE_CONNECTIONS", "0");
-
-        if (allowInsecureConnections == "1") {
-                qWarning() << "Insecure connections are allowed: SSL errors will be ignored";
-                connect(
-                  this,
-                  &QNetworkAccessManager::sslErrors,
-                  this,
-                  [](QNetworkReply *reply, const QList<QSslError> &) { reply->ignoreSslErrors(); });
-        }
-
-        QJsonObject default_filter{
-          {
-            "room",
-            QJsonObject{
-              {"include_leave", true},
-              {
-                "account_data",
-                QJsonObject{
-                  {"not_types", QJsonArray{"*"}},
-                },
-              },
-            },
-          },
-          {
-            "account_data",
-            QJsonObject{
-              {"not_types", QJsonArray{"*"}},
-            },
-          },
-          {
-            "presence",
-            QJsonObject{
-              {"not_types", QJsonArray{"*"}},
-            },
-          },
-        };
-
-        filter_ = settings
-                    .value("client/sync_filter",
-                           QJsonDocument(default_filter).toJson(QJsonDocument::Compact))
-                    .toString();
-
-        connect(this,
-                &QNetworkAccessManager::networkAccessibleChanged,
-                this,
-                [this](NetworkAccessibility status) {
-                        if (status != NetworkAccessibility::Accessible)
-                                setNetworkAccessible(NetworkAccessibility::Accessible);
-                });
-}
-
-void
-MatrixClient::reset() noexcept
-{
-        next_batch_.clear();
-        server_.clear();
-        token_.clear();
-
-        txn_id_ = 0;
-}
-
-void
-MatrixClient::login(const QString &username, const QString &password) noexcept
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/login");
-
-        QNetworkRequest request(endpoint);
-        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-
-        mtx::requests::Login login;
-        login.user                        = username.toStdString();
-        login.password                    = password.toStdString();
-        login.initial_device_display_name = "nheko";
-
-#if defined(Q_OS_MAC)
-        login.initial_device_display_name = "nheko on Mac OS";
-#elif defined(Q_OS_LINUX)
-        login.initial_device_display_name = "nheko on Linux";
-#elif defined(Q_OS_WIN)
-        login.initial_device_display_name = "nheko on Windows";
-#endif
-
-        json j = login;
-
-        auto data  = QByteArray::fromStdString(j.dump());
-        auto reply = post(request, data);
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status_code =
-                  reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status_code == 403) {
-                        emit loginError(tr("Wrong username or password"));
-                        return;
-                }
-
-                if (status_code == 404) {
-                        emit loginError(tr("Login endpoint was not found on the server"));
-                        return;
-                }
-
-                if (status_code >= 400) {
-                        qWarning() << "Login error: " << reply->errorString();
-                        emit loginError(tr("An unknown error occured. Please try again."));
-                        return;
-                }
-
-                if (reply->error()) {
-                        emit loginError(reply->errorString());
-                        return;
-                }
-
-                try {
-                        mtx::responses::Login login =
-                          nlohmann::json::parse(reply->readAll().data());
-
-                        auto hostname = server_.host();
-
-                        if (server_.port() > 0)
-                                hostname = QString("%1:%2").arg(server_.host()).arg(server_.port());
-
-                        emit loginSuccess(QString::fromStdString(login.user_id.to_string()),
-                                          hostname,
-                                          QString::fromStdString(login.access_token));
-                } catch (std::exception &e) {
-                        qWarning() << "Malformed JSON response" << e.what();
-                        emit loginError(tr("Malformed response. Possibly not a Matrix server"));
-                }
-        });
-}
-void
-MatrixClient::logout() noexcept
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/logout");
-
-        QNetworkRequest request(endpoint);
-        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        QJsonObject body{};
-        auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status != 200) {
-                        qWarning() << "Logout error: " << reply->errorString();
-                        return;
-                }
-
-                emit loggedOut();
-        });
-}
-
-void
-MatrixClient::registerUser(const QString &user,
-                           const QString &pass,
-                           const QString &server,
-                           const QString &session) noexcept
-{
-        setServer(server);
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/register");
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-
-        QJsonObject body{{"username", user}, {"password", pass}};
-
-        // We trying to register using the response from the recaptcha.
-        if (!session.isEmpty())
-                body = QJsonObject{
-                  {"username", user},
-                  {"password", pass},
-                  {"auth", QJsonObject{{"type", "m.login.recaptcha"}, {"session", session}}}};
-
-        auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, user, pass, server]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                auto data = reply->readAll();
-
-                // Try to parse a regular register response.
-                try {
-                        mtx::responses::Register res = nlohmann::json::parse(data);
-                        emit registerSuccess(QString::fromStdString(res.user_id.to_string()),
-                                             QString::fromStdString(res.user_id.hostname()),
-                                             QString::fromStdString(res.access_token));
-                } catch (const std::exception &e) {
-                        qWarning() << "Register" << e.what();
-                }
-
-                // Check if the server requires a registration flow.
-                try {
-                        mtx::responses::RegistrationFlows res = nlohmann::json::parse(data);
-                        emit registrationFlow(
-                          user, pass, server, QString::fromStdString(res.session));
-                        return;
-                } catch (const std::exception &) {
-                }
-
-                // We encountered an unknown error.
-                if (status == 0 || status >= 400) {
-                        try {
-                                mtx::errors::Error res = nlohmann::json::parse(data);
-                                emit registerError(QString::fromStdString(res.error));
-                                return;
-                        } catch (const std::exception &) {
-                        }
-
-                        emit registerError(reply->errorString());
-                }
-        });
-}
-
-void
-MatrixClient::sync() noexcept
-{
-        // the filter is not uploaded yet (so it is a json with { at the beginning)
-        // ignore for now that the filter might be uploaded multiple times as we expect
-        // servers to do deduplication
-        if (filter_.startsWith("{")) {
-                uploadFilter(filter_);
-        }
-
-        QUrlQuery query;
-        query.addQueryItem("set_presence", "online");
-        query.addQueryItem("filter", filter_);
-        query.addQueryItem("timeout", "30000");
-
-        if (next_batch_.isEmpty()) {
-                qDebug() << "Sync requires a valid next_batch token. Initial sync should "
-                            "be performed.";
-                return;
-        }
-
-        query.addQueryItem("since", next_batch_);
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/sync");
-        endpoint.setQuery(query);
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        auto reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-                auto data  = reply->readAll();
-
-                if (status == 0 || status >= 400) {
-                        try {
-                                mtx::errors::Error res = nlohmann::json::parse(data);
-
-                                if (res.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) {
-                                        emit invalidToken();
-                                        return;
-                                }
-
-                                emit syncError(QString::fromStdString(res.error));
-
-                                return;
-                        } catch (const nlohmann::json::exception &e) {
-                                qWarning() << e.what();
-                        }
-                }
-
-                try {
-                        emit syncCompleted(nlohmann::json::parse(std::move(data)));
-                } catch (std::exception &e) {
-                        qWarning() << "Sync error: " << e.what();
-                }
-        });
-}
-
-void
-MatrixClient::sendRoomMessage(mtx::events::MessageType ty,
-                              int txnId,
-                              const QString &roomid,
-                              const QString &msg,
-                              const QString &mime,
-                              uint64_t media_size,
-                              const QString &url) noexcept
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ +
-                         QString("/rooms/%1/send/m.room.message/%2").arg(roomid).arg(txnId));
-
-        QJsonObject body;
-        QJsonObject info = {{"size", static_cast<qint64>(media_size)}, {"mimetype", mime}};
-
-        switch (ty) {
-        case mtx::events::MessageType::Text:
-                body = {{"msgtype", "m.text"}, {"body", msg}};
-                break;
-        case mtx::events::MessageType::Emote:
-                body = {{"msgtype", "m.emote"}, {"body", msg}};
-                break;
-        case mtx::events::MessageType::Image:
-                body = {{"msgtype", "m.image"}, {"body", msg}, {"url", url}, {"info", info}};
-                break;
-        case mtx::events::MessageType::File:
-                body = {{"msgtype", "m.file"}, {"body", msg}, {"url", url}, {"info", info}};
-                break;
-        case mtx::events::MessageType::Audio:
-                body = {{"msgtype", "m.audio"}, {"body", msg}, {"url", url}, {"info", info}};
-                break;
-        case mtx::events::MessageType::Video:
-                body = {{"msgtype", "m.video"}, {"body", msg}, {"url", url}, {"info", info}};
-                break;
-        default:
-                qDebug() << "SendRoomMessage: Unknown message type for" << msg;
-                return;
-        }
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        auto reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, txnId]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        emit messageSendFailed(roomid, txnId);
-                        return;
-                }
-
-                auto data = reply->readAll();
-
-                if (data.isEmpty()) {
-                        emit messageSendFailed(roomid, txnId);
-                        return;
-                }
-
-                auto json = QJsonDocument::fromJson(data);
-
-                if (!json.isObject()) {
-                        qDebug() << "Send message response is not a JSON object";
-                        emit messageSendFailed(roomid, txnId);
-                        return;
-                }
-
-                auto object = json.object();
-
-                if (!object.contains("event_id")) {
-                        qDebug() << "SendTextMessage: missing event_id from response";
-                        emit messageSendFailed(roomid, txnId);
-                        return;
-                }
-
-                emit messageSent(object.value("event_id").toString(), roomid, txnId);
-        });
-}
-
-void
-MatrixClient::initialSync() noexcept
-{
-        QUrlQuery query;
-        query.addQueryItem("timeout", "0");
-        query.addQueryItem("filter", filter_);
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/sync");
-        endpoint.setQuery(query);
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        auto reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qDebug() << "Error code received" << status;
-                        emit initialSyncFailed(status);
-                        return;
-                }
-
-                QtConcurrent::run([data = reply->readAll(), this]() {
-                        try {
-                                emit initialSyncCompleted(nlohmann::json::parse(std::move(data)));
-                        } catch (std::exception &e) {
-                                qWarning() << "Initial sync error:" << e.what();
-                                emit initialSyncFailed();
-                        }
-                });
-        });
-}
-
-void
-MatrixClient::versions() noexcept
-{
-        QUrl endpoint(server_);
-        endpoint.setPath("/_matrix/client/versions");
-
-        QNetworkRequest request(endpoint);
-
-        auto reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status_code =
-                  reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (reply->error()) {
-                        emit versionError(reply->errorString());
-                        return;
-                }
-
-                if (status_code == 404) {
-                        emit versionError("Versions endpoint was not found on the server. Possibly "
-                                          "not a Matrix server");
-                        return;
-                }
-
-                if (status_code >= 400) {
-                        emit versionError("An unknown error occured. Please try again.");
-                        return;
-                }
-
-                try {
-                        mtx::responses::Versions versions =
-                          nlohmann::json::parse(reply->readAll().data());
-
-                        emit versionSuccess();
-                } catch (std::exception &e) {
-                        emit versionError("Malformed response. Possibly not a Matrix server");
-                }
-        });
-}
-
-void
-MatrixClient::getOwnProfile() noexcept
-{
-        // FIXME: Remove settings from the matrix client. The class should store the
-        // user's matrix ID.
-        QSettings settings;
-        auto userid = settings.value("auth/user_id", "").toString();
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/profile/" + userid);
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        QNetworkReply *reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                try {
-                        mtx::responses::Profile profile =
-                          nlohmann::json::parse(reply->readAll().data());
-
-                        emit getOwnProfileResponse(QUrl(QString::fromStdString(profile.avatar_url)),
-                                                   QString::fromStdString(profile.display_name));
-                } catch (std::exception &e) {
-                        qWarning() << "Profile:" << e.what();
-                }
-        });
-}
-
-void
-MatrixClient::getOwnCommunities() noexcept
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/joined_groups");
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        QNetworkReply *reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                auto data = reply->readAll();
-                auto json = QJsonDocument::fromJson(data).object();
-
-                if (!json.contains("groups")) {
-                        qWarning() << "failed to parse own communities. 'groups' key not found";
-                        return;
-                }
-
-                QList<QString> response;
-                for (auto group : json["groups"].toArray())
-                        response.append(group.toString());
-
-                emit getOwnCommunitiesResponse(response);
-        });
-}
-
-void
-MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url)
-{
-        QList<QString> url_parts = avatar_url.toString().split("mxc://");
-
-        if (url_parts.size() != 2) {
-                qDebug() << "Invalid format for room avatar " << avatar_url.toString();
-                return;
-        }
-
-        QUrlQuery query;
-        query.addQueryItem("width", "512");
-        query.addQueryItem("height", "512");
-        query.addQueryItem("method", "crop");
-
-        QString media_url =
-          QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
-
-        QUrl endpoint(media_url);
-        endpoint.setQuery(query);
-
-        QNetworkRequest avatar_request(endpoint);
-
-        QNetworkReply *reply = get(avatar_request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, avatar_url]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                auto img = reply->readAll();
-
-                if (img.size() == 0)
-                        return;
-
-                QPixmap pixmap;
-                pixmap.loadFromData(img);
-
-                emit roomAvatarRetrieved(roomid, pixmap, avatar_url.toString(), img);
-        });
-}
-
-void
-MatrixClient::fetchCommunityAvatar(const QString &communityId, const QUrl &avatar_url)
-{
-        if (avatar_url.isEmpty())
-                return;
-
-        QList<QString> url_parts = avatar_url.toString().split("mxc://");
-
-        if (url_parts.size() != 2) {
-                qDebug() << "Invalid format for community avatar " << avatar_url.toString();
-                return;
-        }
-
-        QUrlQuery query;
-        query.addQueryItem("width", "512");
-        query.addQueryItem("height", "512");
-        query.addQueryItem("method", "crop");
-
-        QString media_url =
-          QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
-
-        QUrl endpoint(media_url);
-        endpoint.setQuery(query);
-
-        QNetworkRequest avatar_request(endpoint);
-
-        QNetworkReply *reply = get(avatar_request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                auto img = reply->readAll();
-
-                if (img.size() == 0)
-                        return;
-
-                QPixmap pixmap;
-                pixmap.loadFromData(img);
-
-                emit communityAvatarRetrieved(communityId, pixmap);
-        });
-}
-
-void
-MatrixClient::fetchCommunityProfile(const QString &communityId)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/profile");
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        QNetworkReply *reply = get(request);
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                auto data       = reply->readAll();
-                const auto json = QJsonDocument::fromJson(data).object();
-
-                emit communityProfileRetrieved(communityId, json);
-        });
-}
-
-void
-MatrixClient::fetchCommunityRooms(const QString &communityId)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/rooms");
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        QNetworkReply *reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                auto data       = reply->readAll();
-                const auto json = QJsonDocument::fromJson(data).object();
-
-                emit communityRoomsRetrieved(communityId, json);
-        });
-}
-
-QSharedPointer<DownloadMediaProxy>
-MatrixClient::fetchUserAvatar(const QUrl &avatarUrl)
-{
-        QList<QString> url_parts = avatarUrl.toString().split("mxc://");
-
-        if (url_parts.size() != 2)
-                return QSharedPointer<DownloadMediaProxy>();
-
-        QUrlQuery query;
-        query.addQueryItem("width", "128");
-        query.addQueryItem("height", "128");
-        query.addQueryItem("method", "crop");
-
-        QString media_url =
-          QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
-
-        QUrl endpoint(media_url);
-        endpoint.setQuery(query);
-
-        QNetworkRequest avatar_request(endpoint);
-
-        auto reply = get(avatar_request);
-        auto proxy = QSharedPointer<DownloadMediaProxy>(new DownloadMediaProxy,
-                                                        [](auto proxy) { proxy->deleteLater(); });
-        connect(reply, &QNetworkReply::finished, this, [reply, proxy, avatarUrl]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString() << avatarUrl;
-                        return;
-                }
-
-                auto data = reply->readAll();
-
-                if (data.size() == 0) {
-                        qWarning() << "received avatar with no data:" << avatarUrl;
-                        return;
-                }
-
-                QImage img;
-                img.loadFromData(data);
-
-                emit proxy->avatarDownloaded(img);
-        });
-
-        return proxy;
-}
-
-QSharedPointer<DownloadMediaProxy>
-MatrixClient::downloadImage(const QUrl &url)
-{
-        QNetworkRequest image_request(url);
-
-        auto reply = get(image_request);
-        auto proxy = QSharedPointer<DownloadMediaProxy>(new DownloadMediaProxy,
-                                                        [](auto proxy) { proxy->deleteLater(); });
-        connect(reply, &QNetworkReply::finished, this, [reply, proxy]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                auto img = reply->readAll();
-
-                if (img.size() == 0)
-                        return;
-
-                QPixmap pixmap;
-                pixmap.loadFromData(img);
-
-                emit proxy->imageDownloaded(pixmap);
-        });
-
-        return proxy;
-}
-
-QSharedPointer<DownloadMediaProxy>
-MatrixClient::downloadFile(const QUrl &url)
-{
-        QNetworkRequest fileRequest(url);
-
-        auto reply = get(fileRequest);
-        auto proxy = QSharedPointer<DownloadMediaProxy>(new DownloadMediaProxy,
-                                                        [](auto proxy) { proxy->deleteLater(); });
-        connect(reply, &QNetworkReply::finished, this, [reply, proxy]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        // TODO: Handle error
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                auto data = reply->readAll();
-
-                if (data.size() == 0)
-                        return;
-
-                emit proxy->fileDownloaded(data);
-        });
-
-        return proxy;
-}
-
-void
-MatrixClient::messages(const QString &roomid, const QString &from_token, int limit) noexcept
-{
-        QUrlQuery query;
-        query.addQueryItem("from", from_token);
-        query.addQueryItem("dir", "b");
-        query.addQueryItem("limit", QString::number(limit));
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/messages").arg(roomid));
-        endpoint.setQuery(query);
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        auto reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                try {
-                        mtx::responses::Messages messages =
-                          nlohmann::json::parse(reply->readAll().data());
-
-                        emit messagesRetrieved(roomid, messages);
-                } catch (std::exception &e) {
-                        qWarning() << "Room messages from" << roomid << e.what();
-                        return;
-                }
-        });
-}
-
-void
-MatrixClient::uploadImage(const QString &roomid,
-                          const QString &filename,
-                          const QSharedPointer<QIODevice> data)
-{
-        auto reply = makeUploadRequest(data);
-
-        if (reply == nullptr)
-                return;
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
-                auto json = getUploadReply(reply);
-                if (json.isEmpty())
-                        return;
-
-                auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
-                auto size =
-                  reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
-                emit imageUploaded(
-                  roomid, filename, json.value("content_uri").toString(), mime, size);
-        });
-}
-
-void
-MatrixClient::uploadFile(const QString &roomid,
-                         const QString &filename,
-                         const QSharedPointer<QIODevice> data)
-{
-        auto reply = makeUploadRequest(data);
-
-        if (reply == nullptr)
-                return;
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
-                auto json = getUploadReply(reply);
-                if (json.isEmpty())
-                        return;
-
-                auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
-                auto size =
-                  reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
-                emit fileUploaded(
-                  roomid, filename, json.value("content_uri").toString(), mime, size);
-        });
-}
-
-void
-MatrixClient::uploadAudio(const QString &roomid,
-                          const QString &filename,
-                          const QSharedPointer<QIODevice> data)
-{
-        auto reply = makeUploadRequest(data);
-
-        if (reply == nullptr)
-                return;
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
-                auto json = getUploadReply(reply);
-                if (json.isEmpty())
-                        return;
-
-                auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
-                auto size =
-                  reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
-                emit audioUploaded(
-                  roomid, filename, json.value("content_uri").toString(), mime, size);
-        });
-}
-
-void
-MatrixClient::uploadVideo(const QString &roomid,
-                          const QString &filename,
-                          const QSharedPointer<QIODevice> data)
-{
-        auto reply = makeUploadRequest(data);
-
-        if (reply == nullptr)
-                return;
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() {
-                auto json = getUploadReply(reply);
-                if (json.isEmpty())
-                        return;
-
-                auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString();
-                auto size =
-                  reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong();
-
-                emit videoUploaded(
-                  roomid, filename, json.value("content_uri").toString(), mime, size);
-        });
-}
-
-void
-MatrixClient::uploadFilter(const QString &filter) noexcept
-{
-        // validate that filter is a Json-String
-        QJsonDocument doc = QJsonDocument::fromJson(filter.toUtf8());
-        if (doc.isNull() || !doc.isObject()) {
-                qWarning() << "Input which should be uploaded as filter is no JsonObject";
-                return;
-        }
-
-        QSettings settings;
-        auto userid = settings.value("auth/user_id", "").toString();
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/user/%1/filter").arg(userid));
-
-        QNetworkRequest request(endpoint);
-        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        auto reply = post(request, doc.toJson(QJsonDocument::Compact));
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString() << "42";
-                        return;
-                }
-
-                auto data      = reply->readAll();
-                auto response  = QJsonDocument::fromJson(data);
-                auto filter_id = response.object()["filter_id"].toString();
-
-                qDebug() << "Filter with ID" << filter_id << "created.";
-                QSettings settings;
-                settings.setValue("client/sync_filter", filter_id);
-                settings.sync();
-
-                // set the filter_ var so following syncs will use it
-                filter_ = filter_id;
-        });
-}
-
-void
-MatrixClient::joinRoom(const QString &roomIdOrAlias)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/join/%1").arg(roomIdOrAlias));
-
-        QNetworkRequest request(endpoint);
-        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        auto reply = post(request, "{}");
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        auto data     = reply->readAll();
-                        auto response = QJsonDocument::fromJson(data);
-                        auto json     = response.object();
-
-                        if (json.contains("error"))
-                                emit joinFailed(json["error"].toString());
-                        else
-                                qDebug() << reply->errorString();
-
-                        return;
-                }
-
-                auto data     = reply->readAll();
-                auto response = QJsonDocument::fromJson(data);
-                auto room_id  = response.object()["room_id"].toString();
-
-                emit joinedRoom(room_id);
-        });
-}
-
-void
-MatrixClient::leaveRoom(const QString &roomId)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/leave").arg(roomId));
-
-        QNetworkRequest request(endpoint);
-        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        auto reply = post(request, "{}");
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomId]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                emit leftRoom(roomId);
-        });
+        return v2_client_.get();
 }
 
-void
-MatrixClient::inviteUser(const QString &roomId, const QString &user)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/invite").arg(roomId));
-
-        QNetworkRequest request(endpoint);
-        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        QJsonObject body{{"user_id", user}};
-        auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomId, user]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        // TODO: Handle failure.
-                        qWarning() << reply->errorString();
-                        return;
-                }
-
-                emit invitedUser(roomId, user);
-        });
-}
+} // namespace v2
 
 void
-MatrixClient::createRoom(const mtx::requests::CreateRoom &create_room_request)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/createRoom"));
-
-        QNetworkRequest request(endpoint);
-        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        nlohmann::json body = create_room_request;
-        auto reply          = post(request, QString::fromStdString(body.dump()).toUtf8());
-
-        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        auto data     = reply->readAll();
-                        auto response = QJsonDocument::fromJson(data);
-                        auto json     = response.object();
-
-                        if (json.contains("error"))
-                                emit roomCreationFailed(json["error"].toString());
-                        else
-                                qDebug() << reply->errorString();
-
-                        return;
-                }
-
-                auto data     = reply->readAll();
-                auto response = QJsonDocument::fromJson(data);
-                auto room_id  = response.object()["room_id"].toString();
-
-                emit roomCreated(room_id);
-        });
-}
-
-void
-MatrixClient::sendTypingNotification(const QString &roomid, int timeoutInMillis)
-{
-        QSettings settings;
-        QString user_id = settings.value("auth/user_id").toString();
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/typing/%2").arg(roomid).arg(user_id));
-
-        QString msgType("");
-        QJsonObject body;
-
-        body = {{"typing", true}, {"timeout", timeoutInMillis}};
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-}
-
-void
-MatrixClient::removeTypingNotification(const QString &roomid)
-{
-        QSettings settings;
-        QString user_id = settings.value("auth/user_id").toString();
-
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/typing/%2").arg(roomid).arg(user_id));
-
-        QString msgType("");
-        QJsonObject body;
-
-        body = {{"typing", false}};
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-}
-
-void
-MatrixClient::readEvent(const QString &room_id, const QString &event_id)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/read_markers").arg(room_id));
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        QJsonObject body({{"m.fully_read", event_id}, {"m.read", event_id}});
-        auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
-        connect(reply, &QNetworkReply::finished, this, [reply]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-                if (status == 0 || status >= 400) {
-                        qWarning() << reply->errorString();
-                        return;
-                }
-        });
-}
-
-QNetworkReply *
-MatrixClient::makeUploadRequest(QSharedPointer<QIODevice> iodev)
-{
-        QUrl endpoint(server_);
-        endpoint.setPath(mediaApiUrl_ + "/upload");
-
-        if (!iodev->open(QIODevice::ReadOnly)) {
-                qWarning() << "Error while reading device:" << iodev->errorString();
-                return nullptr;
-        }
-
-        QMimeDatabase db;
-        QMimeType mime = db.mimeTypeForData(iodev.data());
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::ContentTypeHeader, mime.name());
-        setupAuth(request);
-
-        auto reply = post(request, iodev.data());
-
-        return reply;
-}
-
-QJsonObject
-MatrixClient::getUploadReply(QNetworkReply *reply)
-{
-        QJsonObject object;
-
-        reply->deleteLater();
-
-        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-        if (status == 0 || status >= 400) {
-                emit uploadFailed(status,
-                                  QString("Media upload failed - %1").arg(reply->errorString()));
-                return object;
-        }
-
-        auto res_data = reply->readAll();
-
-        if (res_data.isEmpty()) {
-                emit uploadFailed(status, "Media upload failed - Empty response");
-                return object;
-        }
-
-        auto json = QJsonDocument::fromJson(res_data);
-
-        if (!json.isObject()) {
-                emit uploadFailed(status, "Media upload failed - Invalid response");
-                return object;
-        }
-
-        object = json.object();
-        if (!object.contains("content_uri")) {
-                emit uploadFailed(status, "Media upload failed - Missing 'content_uri'");
-                return QJsonObject{};
-        }
-
-        return object;
-}
-
-void
-MatrixClient::redactEvent(const QString &room_id, const QString &event_id)
+init()
 {
-        QUrl endpoint(server_);
-        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/redact/%2/%3")
-                                           .arg(room_id)
-                                           .arg(event_id)
-                                           .arg(incrementTransactionId()));
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
-        setupAuth(request);
-
-        // TODO: no reason specified
-        QJsonObject body{};
-        auto reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
-
-        connect(reply, &QNetworkReply::finished, this, [reply, this, room_id, event_id]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-                auto data  = reply->readAll();
-
-                if (status == 0 || status >= 400) {
-                        try {
-                                mtx::errors::Error res = nlohmann::json::parse(data);
-                                emit redactionFailed(QString::fromStdString(res.error));
-                                return;
-                        } catch (const std::exception &) {
-                        }
-                }
-
-                try {
-                        mtx::responses::EventId res = nlohmann::json::parse(data);
-                        emit redactionCompleted(room_id, event_id);
-                } catch (const std::exception &e) {
-                        emit redactionFailed(QString::fromStdString(e.what()));
-                }
-        });
+        qRegisterMetaType<mtx::responses::Login>();
+        qRegisterMetaType<mtx::responses::Messages>();
+        qRegisterMetaType<mtx::responses::Notifications>();
+        qRegisterMetaType<mtx::responses::Rooms>();
+        qRegisterMetaType<mtx::responses::Sync>();
+        qRegisterMetaType<std::string>();
+        qRegisterMetaType<std::vector<std::string>>();
 }
 
-void
-MatrixClient::getNotifications() noexcept
-{
-        QUrlQuery query;
-        query.addQueryItem("limit", "5");
-
-        QUrl endpoint(server_);
-        endpoint.setQuery(query);
-        endpoint.setPath(clientApiUrl_ + "/notifications");
-
-        QNetworkRequest request(QString(endpoint.toEncoded()));
-        setupAuth(request);
-
-        auto reply = get(request);
-        connect(reply, &QNetworkReply::finished, this, [reply, this]() {
-                reply->deleteLater();
-
-                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-                auto data  = reply->readAll();
-
-                if (status == 0 || status >= 400) {
-                        try {
-                                mtx::errors::Error res = nlohmann::json::parse(data);
-                                std::cout << nlohmann::json::parse(data).dump(2) << '\n';
-                                // TODO: Response with an error signal
-                                return;
-                        } catch (const std::exception &) {
-                        }
-                }
-
-                try {
-                        emit notificationsRetrieved(nlohmann::json::parse(data));
-                } catch (const std::exception &e) {
-                        qWarning() << "failed to parse /notifications response" << e.what();
-                }
-        });
-}
+} // namespace http
diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc
index 7d80b7274..a5960b831 100644
--- a/src/RegisterPage.cc
+++ b/src/RegisterPage.cc
@@ -20,6 +20,7 @@
 
 #include "Config.h"
 #include "FlatButton.h"
+#include "Logging.hpp"
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "RaisedButton.h"
@@ -125,35 +126,53 @@ RegisterPage::RegisterPage(QWidget *parent)
         connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
         connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
         connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(http::client(),
-                SIGNAL(registerError(const QString &)),
-                this,
-                SLOT(registerError(const QString &)));
-        connect(http::client(),
-                &MatrixClient::registrationFlow,
-                this,
-                [this](const QString &user,
-                       const QString &pass,
-                       const QString &server,
-                       const QString &session) {
-                        emit errorOccurred();
-
-                        if (!captchaDialog_) {
-                                captchaDialog_ =
-                                  std::make_shared<dialogs::ReCaptcha>(server, session, this);
-                                connect(captchaDialog_.get(),
-                                        &dialogs::ReCaptcha::closing,
-                                        this,
-                                        [this, user, pass, server, session]() {
-                                                captchaDialog_->close();
-                                                emit registering();
-                                                http::client()->registerUser(
-                                                  user, pass, server, session);
-                                        });
-                        }
-
-                        QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); });
-                });
+        connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError);
+        connect(
+          this,
+          &RegisterPage::registrationFlow,
+          this,
+          [this](const std::string &user, const std::string &pass, const std::string &session) {
+                  emit errorOccurred();
+
+                  if (!captchaDialog_) {
+                          captchaDialog_ = std::make_shared<dialogs::ReCaptcha>(
+                            QString::fromStdString(session), this);
+                          connect(
+                            captchaDialog_.get(),
+                            &dialogs::ReCaptcha::closing,
+                            this,
+                            [this, user, pass, session]() {
+                                    captchaDialog_->close();
+                                    emit registering();
+
+                                    http::v2::client()->flow_response(
+                                      user,
+                                      pass,
+                                      session,
+                                      "m.login.recaptcha",
+                                      [this](const mtx::responses::Register &res,
+                                             mtx::http::RequestErr err) {
+                                              if (err) {
+                                                      log::net()->warn(
+                                                        "failed to retrieve registration flows: {}",
+                                                        err->matrix_error.error);
+                                                      emit errorOccurred();
+                                                      emit registerErrorCb(QString::fromStdString(
+                                                        err->matrix_error.error));
+                                                      return;
+                                              }
+
+                                              http::v2::client()->set_user(res.user_id);
+                                              http::v2::client()->set_access_token(
+                                                res.access_token);
+
+                                              emit registerOk();
+                                      });
+                            });
+                  }
+
+                  QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); });
+          });
 
         setLayout(top_layout_);
 }
@@ -185,11 +204,56 @@ RegisterPage::onRegisterButtonClicked()
         } else if (!server_input_->hasAcceptableInput()) {
                 registerError(tr("Invalid server name"));
         } else {
-                QString username = username_input_->text();
-                QString password = password_input_->text();
-                QString server   = server_input_->text();
+                auto username = username_input_->text().toStdString();
+                auto password = password_input_->text().toStdString();
+                auto server   = server_input_->text().toStdString();
+
+                http::v2::client()->set_server(server);
+                http::v2::client()->registration(
+                  username,
+                  password,
+                  [this, username, password](const mtx::responses::Register &res,
+                                             mtx::http::RequestErr err) {
+                          if (!err) {
+                                  http::v2::client()->set_user(res.user_id);
+                                  http::v2::client()->set_access_token(res.access_token);
+
+                                  emit registerOk();
+                                  return;
+                          }
+
+                          // The server requires registration flows.
+                          if (err->status_code == boost::beast::http::status::unauthorized) {
+                                  http::v2::client()->flow_register(
+                                    username,
+                                    password,
+                                    [this, username, password](
+                                      const mtx::responses::RegistrationFlows &res,
+                                      mtx::http::RequestErr err) {
+                                            if (res.session.empty() && err) {
+                                                    log::net()->warn(
+                                                      "failed to retrieve registration flows: ({}) "
+                                                      "{}",
+                                                      static_cast<int>(err->status_code),
+                                                      err->matrix_error.error);
+                                                    emit errorOccurred();
+                                                    emit registerErrorCb(QString::fromStdString(
+                                                      err->matrix_error.error));
+                                                    return;
+                                            }
+
+                                            emit registrationFlow(username, password, res.session);
+                                    });
+                                  return;
+                          }
+
+                          log::net()->warn("failed to register: status_code ({})",
+                                           static_cast<int>(err->status_code));
+
+                          emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
+                          emit errorOccurred();
+                  });
 
-                http::client()->registerUser(username, password, server);
                 emit registering();
         }
 }
diff --git a/src/RoomList.cc b/src/RoomList.cc
index e7c5ef308..d3ed2e665 100644
--- a/src/RoomList.cc
+++ b/src/RoomList.cc
@@ -16,11 +16,11 @@
  */
 
 #include <QBuffer>
-#include <QDebug>
 #include <QObject>
 #include <QTimer>
 
 #include "Cache.h"
+#include "Logging.hpp"
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "OverlayModal.h"
@@ -55,18 +55,7 @@ RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         scrollArea_->setWidget(scrollAreaContents_);
         topLayout_->addWidget(scrollArea_);
 
-        connect(http::client(),
-                &MatrixClient::roomAvatarRetrieved,
-                this,
-                [this](const QString &room_id,
-                       const QPixmap &img,
-                       const QString &url,
-                       const QByteArray &data) {
-                        if (cache::client())
-                                cache::client()->saveImage(url, data);
-
-                        updateRoomAvatar(room_id, img);
-                });
+        connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar);
 }
 
 void
@@ -101,7 +90,28 @@ RoomList::updateAvatar(const QString &room_id, const QString &url)
                 savedImgData = cache::client()->image(url);
 
         if (savedImgData.isEmpty()) {
-                http::client()->fetchRoomAvatar(room_id, url);
+                mtx::http::ThumbOpts opts;
+                opts.mxc_url = url.toStdString();
+                http::v2::client()->get_thumbnail(
+                  opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) {
+                          if (err) {
+                                  log::net()->warn(
+                                    "failed to download thumbnail: {}, {} - {}",
+                                    opts.mxc_url,
+                                    mtx::errors::to_string(err->matrix_error.errcode),
+                                    err->matrix_error.error);
+                                  return;
+                          }
+
+                          if (cache::client())
+                                  cache::client()->saveImage(opts.mxc_url, res);
+
+                          auto data = QByteArray(res.data(), res.size());
+                          QPixmap pixmap;
+                          pixmap.loadFromData(data);
+
+                          emit updateRoomAvatarCb(room_id, pixmap);
+                  });
         } else {
                 QPixmap img;
                 img.loadFromData(savedImgData);
@@ -131,7 +141,8 @@ void
 RoomList::updateUnreadMessageCount(const QString &roomid, int count)
 {
         if (!roomExists(roomid)) {
-                qWarning() << "UpdateUnreadMessageCount: Unknown roomid";
+                log::main()->warn("updateUnreadMessageCount: unknown room_id {}",
+                                  roomid.toStdString());
                 return;
         }
 
@@ -156,7 +167,7 @@ RoomList::calculateUnreadMessageCount()
 void
 RoomList::initialize(const QMap<QString, RoomInfo> &info)
 {
-        qDebug() << "initialize room list";
+        log::main()->info("initialize room list");
 
         rooms_.clear();
 
@@ -209,7 +220,7 @@ RoomList::highlightSelectedRoom(const QString &room_id)
         emit roomChanged(room_id);
 
         if (!roomExists(room_id)) {
-                qDebug() << "RoomList: clicked unknown roomid";
+                log::main()->warn("roomlist: clicked unknown room_id");
                 return;
         }
 
@@ -232,7 +243,8 @@ void
 RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img)
 {
         if (!roomExists(roomid)) {
-                qWarning() << "Avatar update on non existent room" << roomid;
+                log::main()->warn("avatar update on non-existent room_id: {}",
+                                  roomid.toStdString());
                 return;
         }
 
@@ -246,7 +258,9 @@ void
 RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
 {
         if (!roomExists(roomid)) {
-                qWarning() << "Description update on non existent room" << roomid << info.body;
+                log::main()->warn("description update on non-existent room_id: {}, {}",
+                                  roomid.toStdString(),
+                                  info.body.toStdString());
                 return;
         }
 
@@ -314,7 +328,7 @@ RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias)
         joinRoomModal_->hide();
 
         if (isJoining)
-                http::client()->joinRoom(roomAlias);
+                emit joinRoom(roomAlias);
 }
 
 void
diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc
index f37539715..acb33fa73 100644
--- a/src/TextInputWidget.cc
+++ b/src/TextInputWidget.cc
@@ -71,8 +71,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
                 this,
                 &FilteredTextEdit::uploadData);
 
-        qRegisterMetaType<SearchResult>();
-        qRegisterMetaType<QVector<SearchResult>>();
         connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
         connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
                 popup_.hide();
diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp
index ba487cea5..6b1143b5a 100644
--- a/src/dialogs/ReCaptcha.cpp
+++ b/src/dialogs/ReCaptcha.cpp
@@ -6,6 +6,7 @@
 
 #include "Config.h"
 #include "FlatButton.h"
+#include "MatrixClient.h"
 #include "RaisedButton.h"
 #include "Theme.h"
 
@@ -13,7 +14,7 @@
 
 using namespace dialogs;
 
-ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *parent)
+ReCaptcha::ReCaptcha(const QString &session, QWidget *parent)
   : QWidget(parent)
 {
         setAutoFillBackground(true);
@@ -51,12 +52,12 @@ ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *par
         layout->addWidget(label);
         layout->addLayout(buttonLayout);
 
-        connect(openCaptchaBtn_, &QPushButton::clicked, [server, session]() {
-                const auto url =
-                  QString(
-                    "https://%1/_matrix/client/r0/auth/m.login.recaptcha/fallback/web?session=%2")
-                    .arg(server)
-                    .arg(session);
+        connect(openCaptchaBtn_, &QPushButton::clicked, [session]() {
+                const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/"
+                                         "fallback/web?session=%3")
+                                   .arg(QString::fromStdString(http::v2::client()->server()))
+                                   .arg(http::v2::client()->port())
+                                   .arg(session);
 
                 QDesktopServices::openUrl(url);
         });
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
index 4d2f304b9..2396fc194 100644
--- a/src/dialogs/RoomSettings.cpp
+++ b/src/dialogs/RoomSettings.cpp
@@ -67,6 +67,20 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
         labelLayout->addWidget(errorField_);
         layout->addLayout(labelLayout);
 
+        connect(this, &EditModal::stateEventErrorCb, this, [this](const QString &msg) {
+                errorField_->setText(msg);
+                errorField_->show();
+        });
+        connect(this, &EditModal::nameEventSentCb, this, [this](const QString &newName) {
+                errorField_->hide();
+                emit nameChanged(newName);
+                close();
+        });
+        connect(this, &EditModal::topicEventSentCb, this, [this]() {
+                errorField_->hide();
+                close();
+        });
+
         connect(applyBtn_, &QPushButton::clicked, [this]() {
                 // Check if the values are changed from the originals.
                 auto newName  = nameInput_->text().trimmed();
@@ -85,53 +99,37 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
                         state::Name body;
                         body.name = newName.toStdString();
 
-                        auto proxy =
-                          http::client()->sendStateEvent<state::Name, EventType::RoomName>(body,
-                                                                                           roomId_);
-                        connect(proxy.get(),
-                                &StateEventProxy::stateEventSent,
-                                this,
-                                [this, proxy, newName]() {
-                                        Q_UNUSED(proxy);
-                                        errorField_->hide();
-                                        emit nameChanged(newName);
-                                        close();
-                                });
-
-                        connect(proxy.get(),
-                                &StateEventProxy::stateEventError,
-                                this,
-                                [this, proxy, newName](const QString &msg) {
-                                        Q_UNUSED(proxy);
-                                        errorField_->setText(msg);
-                                        errorField_->show();
-                                });
+                        http::v2::client()->send_state_event<state::Name, EventType::RoomName>(
+                          roomId_.toStdString(),
+                          body,
+                          [this, newName](const mtx::responses::EventId &,
+                                          mtx::http::RequestErr err) {
+                                  if (err) {
+                                          emit stateEventErrorCb(
+                                            QString::fromStdString(err->matrix_error.error));
+                                          return;
+                                  }
+
+                                  emit nameEventSentCb(newName);
+                          });
                 }
 
                 if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
                         state::Topic body;
                         body.topic = newTopic.toStdString();
 
-                        auto proxy =
-                          http::client()->sendStateEvent<state::Topic, EventType::RoomTopic>(
-                            body, roomId_);
-                        connect(proxy.get(),
-                                &StateEventProxy::stateEventSent,
-                                this,
-                                [this, proxy, newTopic]() {
-                                        Q_UNUSED(proxy);
-                                        errorField_->hide();
-                                        close();
-                                });
-
-                        connect(proxy.get(),
-                                &StateEventProxy::stateEventError,
-                                this,
-                                [this, proxy, newTopic](const QString &msg) {
-                                        Q_UNUSED(proxy);
-                                        errorField_->setText(msg);
-                                        errorField_->show();
-                                });
+                        http::v2::client()->send_state_event<state::Topic, EventType::RoomTopic>(
+                          roomId_.toStdString(),
+                          body,
+                          [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+                                  if (err) {
+                                          emit stateEventErrorCb(
+                                            QString::fromStdString(err->matrix_error.error));
+                                          return;
+                                  }
+
+                                  emit topicEventSentCb();
+                          });
                 }
         });
         connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
diff --git a/src/main.cc b/src/main.cc
index bd3a212ce..1df8d0c9f 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -22,15 +22,17 @@
 #include <QLabel>
 #include <QLayout>
 #include <QLibraryInfo>
-#include <QNetworkProxy>
 #include <QPalette>
 #include <QPoint>
 #include <QPushButton>
 #include <QSettings>
+#include <QStandardPaths>
 #include <QTranslator>
 
 #include "Config.h"
+#include "Logging.hpp"
 #include "MainWindow.h"
+#include "MatrixClient.h"
 #include "RaisedButton.h"
 #include "RunGuard.h"
 #include "version.hpp"
@@ -46,32 +48,6 @@ screenCenter(int width, int height)
         return QPoint(x, y);
 }
 
-void
-setupProxy()
-{
-        QSettings settings;
-
-        /**
-          To set up a SOCKS proxy:
-            [user]
-            proxy\socks\host=<>
-            proxy\socks\port=<>
-            proxy\socks\user=<>
-            proxy\socks\password=<>
-          **/
-        if (settings.contains("user/proxy/socks/host")) {
-                QNetworkProxy proxy;
-                proxy.setType(QNetworkProxy::Socks5Proxy);
-                proxy.setHostName(settings.value("user/proxy/socks/host").toString());
-                proxy.setPort(settings.value("user/proxy/socks/port").toInt());
-                if (settings.contains("user/proxy/socks/user"))
-                        proxy.setUser(settings.value("user/proxy/socks/user").toString());
-                if (settings.contains("user/proxy/socks/password"))
-                        proxy.setPassword(settings.value("user/proxy/socks/password").toString());
-                QNetworkProxy::setApplicationProxy(proxy);
-        }
-}
-
 int
 main(int argc, char *argv[])
 {
@@ -133,7 +109,17 @@ main(int argc, char *argv[])
         QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf");
 
         app.setWindowIcon(QIcon(":/logos/nheko.png"));
-        qSetMessagePattern("%{time process}: [%{type}] - %{message}");
+
+        http::init();
+
+        try {
+                log::init(QString("%1/nheko.log")
+                            .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                            .toStdString());
+        } catch (const spdlog::spdlog_ex &ex) {
+                std::cout << "Log initialization failed: " << ex.what() << std::endl;
+                std::exit(1);
+        }
 
         QSettings settings;
 
@@ -154,8 +140,6 @@ main(int argc, char *argv[])
         appTranslator.load("nheko_" + lang, ":/translations");
         app.installTranslator(&appTranslator);
 
-        setupProxy();
-
         MainWindow w;
 
         // Move the MainWindow to the center
@@ -167,5 +151,7 @@ main(int argc, char *argv[])
 
         QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize);
 
+        log::main()->info("starting nheko {}", nheko::version);
+
         return app.exec();
 }
diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc
index 250373e47..83a0aaed7 100644
--- a/src/timeline/TimelineItem.cc
+++ b/src/timeline/TimelineItem.cc
@@ -62,9 +62,27 @@ TimelineItem::init()
                         ChatPage::instance()->showReadReceipts(event_id_);
         });
 
+        connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {
+                emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);
+        });
+        connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {
+                emit ChatPage::instance()->showNotification(msg);
+        });
         connect(redactMsg_, &QAction::triggered, this, [this]() {
                 if (!event_id_.isEmpty())
-                        http::client()->redactEvent(room_id_, event_id_);
+                        http::v2::client()->redact_event(
+                          room_id_.toStdString(),
+                          event_id_.toStdString(),
+                          [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+                                  if (err) {
+                                          emit redactionFailed(tr("Message redaction failed: %1")
+                                                                 .arg(QString::fromStdString(
+                                                                   err->matrix_error.error)));
+                                          return;
+                                  }
+
+                                  emit eventRedacted(event_id_);
+                          });
         });
 
         connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); });
diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc
index 71058d741..5ef390a98 100644
--- a/src/timeline/TimelineView.cc
+++ b/src/timeline/TimelineView.cc
@@ -23,6 +23,7 @@
 #include "ChatPage.h"
 #include "Config.h"
 #include "FloatingButton.h"
+#include "Logging.hpp"
 #include "UserSettingsPage.h"
 #include "Utils.h"
 
@@ -100,7 +101,7 @@ TimelineView::TimelineView(const QString &room_id, QWidget *parent)
   , room_id_{room_id}
 {
         init();
-        http::client()->messages(room_id_, "");
+        getMessages();
 }
 
 void
@@ -140,7 +141,7 @@ TimelineView::fetchHistory()
                         return;
 
                 isPaginationInProgress_ = true;
-                http::client()->messages(room_id_, prev_batch_token_);
+                getMessages();
                 paginationTimer_->start(5000);
 
                 return;
@@ -189,18 +190,13 @@ TimelineView::sliderMoved(int position)
 
                 isPaginationInProgress_ = true;
 
-                // FIXME: Maybe move this to TimelineViewManager to remove the
-                // extra calls?
-                http::client()->messages(room_id_, prev_batch_token_);
+                getMessages();
         }
 }
 
 void
-TimelineView::addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs)
+TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs)
 {
-        if (room_id_ != room_id)
-                return;
-
         // We've reached the start of the timline and there're no more messages.
         if ((msgs.end == msgs.start) && msgs.chunk.size() == 0) {
                 isTimelineFinished = true;
@@ -427,10 +423,10 @@ TimelineView::init()
         paginationTimer_ = new QTimer(this);
         connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
 
-        connect(http::client(),
-                &MatrixClient::messagesRetrieved,
-                this,
-                &TimelineView::addBackwardsEvents);
+        connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents);
+
+        connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
+        connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage);
 
         connect(scroll_area_->verticalScrollBar(),
                 SIGNAL(valueChanged(int)),
@@ -442,6 +438,27 @@ TimelineView::init()
                 SLOT(sliderRangeChanged(int, int)));
 }
 
+void
+TimelineView::getMessages()
+{
+        mtx::http::MessagesOpts opts;
+        opts.room_id = room_id_.toStdString();
+        opts.from    = prev_batch_token_.toStdString();
+
+        http::v2::client()->messages(
+          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          log::net()->error("failed to call /messages ({}): {} - {}",
+                                            opts.room_id,
+                                            mtx::errors::to_string(err->matrix_error.errcode),
+                                            err->matrix_error.error);
+                          return;
+                  }
+
+                  emit messagesRetrieved(std::move(res));
+          });
+}
+
 void
 TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
 {
@@ -513,7 +530,7 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction)
 }
 
 void
-TimelineView::updatePendingMessage(int txn_id, QString event_id)
+TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id)
 {
         if (!pending_msgs_.isEmpty() &&
             pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
@@ -548,8 +565,11 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
 
         saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
 
-        int txn_id = http::client()->incrementTransactionId();
-        PendingMessage message(ty, txn_id, body, "", "", -1, "", view_item);
+        PendingMessage message;
+        message.ty     = ty;
+        message.txn_id = mtx::client::utils::random_token();
+        message.body   = body;
+        message.widget = view_item;
         handleNewUserMessage(message);
 }
 
@@ -567,19 +587,119 @@ TimelineView::sendNextPendingMessage()
         if (pending_msgs_.size() == 0)
                 return;
 
+        using namespace mtx::events;
+
         PendingMessage &m = pending_msgs_.head();
         switch (m.ty) {
-        case mtx::events::MessageType::Audio:
-        case mtx::events::MessageType::Image:
-        case mtx::events::MessageType::Video:
-        case mtx::events::MessageType::File:
-                // FIXME: Improve the API
-                http::client()->sendRoomMessage(
-                  m.ty, m.txn_id, room_id_, m.filename, m.mime, m.media_size, m.body);
+        case mtx::events::MessageType::Audio: {
+                msg::Audio audio;
+                audio.info.mimetype = m.mime.toStdString();
+                audio.info.size     = m.media_size;
+                audio.body          = m.filename.toStdString();
+                audio.url           = m.body.toStdString();
+
+                http::v2::client()->send_room_message<msg::Audio, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  audio,
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::Image: {
+                msg::Image image;
+                image.info.mimetype = m.mime.toStdString();
+                image.info.size     = m.media_size;
+                image.body          = m.filename.toStdString();
+                image.url           = m.body.toStdString();
+
+                http::v2::client()->send_room_message<msg::Image, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  image,
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::Video: {
+                msg::Video video;
+                video.info.mimetype = m.mime.toStdString();
+                video.info.size     = m.media_size;
+                video.body          = m.filename.toStdString();
+                video.url           = m.body.toStdString();
+
+                http::v2::client()->send_room_message<msg::Video, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  video,
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::File: {
+                msg::File file;
+                file.info.mimetype = m.mime.toStdString();
+                file.info.size     = m.media_size;
+                file.body          = m.filename.toStdString();
+                file.url           = m.body.toStdString();
+
+                http::v2::client()->send_room_message<msg::File, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  file,
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::Text: {
+                msg::Text text;
+                text.body = m.body.toStdString();
+
+                http::v2::client()->send_room_message<msg::Text, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  text,
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::Emote: {
+                msg::Emote emote;
+                emote.body = m.body.toStdString();
+
+                http::v2::client()->send_room_message<msg::Emote, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  emote,
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
                 break;
+        }
         default:
-                http::client()->sendRoomMessage(
-                  m.ty, m.txn_id, room_id_, m.body, m.mime, m.media_size);
+                log::main()->warn("cannot send unknown message type: {}", m.body.toStdString());
                 break;
         }
 }
@@ -593,7 +713,7 @@ TimelineView::notifyForLastEvent()
         if (lastTimelineItem)
                 emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
         else
-                qWarning() << "Cast to TimelineView failed" << room_id_;
+                log::main()->warn("cast to TimelineView failed: {}", room_id_.toStdString());
 }
 
 void
@@ -606,29 +726,27 @@ TimelineView::notifyForLastEvent(const TimelineEvent &event)
 }
 
 bool
-TimelineView::isPendingMessage(const QString &txnid,
+TimelineView::isPendingMessage(const std::string &txn_id,
                                const QString &sender,
                                const QString &local_userid)
 {
         if (sender != local_userid)
                 return false;
 
-        auto match_txnid = [txnid](const auto &msg) -> bool {
-                return QString::number(msg.txn_id) == txnid;
-        };
+        auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; };
 
         return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) ||
                std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid);
 }
 
 void
-TimelineView::removePendingMessage(const QString &txnid)
+TimelineView::removePendingMessage(const std::string &txn_id)
 {
-        if (txnid.isEmpty())
+        if (txn_id.empty())
                 return;
 
         for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
-                if (QString::number(it->txn_id) == txnid) {
+                if (it->txn_id == txn_id) {
                         int index = std::distance(pending_sent_msgs_.begin(), it);
                         pending_sent_msgs_.removeAt(index);
 
@@ -639,7 +757,7 @@ TimelineView::removePendingMessage(const QString &txnid)
                 }
         }
         for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
-                if (QString::number(it->txn_id) == txnid) {
+                if (it->txn_id == txn_id) {
                         int index = std::distance(pending_msgs_.begin(), it);
                         pending_msgs_.removeAt(index);
                         return;
@@ -648,9 +766,9 @@ TimelineView::removePendingMessage(const QString &txnid)
 }
 
 void
-TimelineView::handleFailedMessage(int txnid)
+TimelineView::handleFailedMessage(const std::string &txn_id)
 {
-        Q_UNUSED(txnid);
+        Q_UNUSED(txn_id);
         // Note: We do this even if the message has already been echoed.
         QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage()));
 }
@@ -673,7 +791,16 @@ TimelineView::readLastEvent() const
         const auto eventId = getLastEventId();
 
         if (!eventId.isEmpty())
-                http::client()->readEvent(room_id_, eventId);
+                http::v2::client()->read_event(room_id_.toStdString(),
+                                               eventId.toStdString(),
+                                               [this, eventId](mtx::http::RequestErr err) {
+                                                       if (err) {
+                                                               log::net()->warn(
+                                                                 "failed to read event ({}, {})",
+                                                                 room_id_.toStdString(),
+                                                                 eventId.toStdString());
+                                                       }
+                                               });
 }
 
 QString
@@ -743,7 +870,8 @@ void
 TimelineView::removeEvent(const QString &event_id)
 {
         if (!eventIds_.contains(event_id)) {
-                qWarning() << "unknown event_id couldn't be removed:" << event_id;
+                log::main()->warn("cannot remove widget with unknown event_id: {}",
+                                  event_id.toStdString());
                 return;
         }
 
@@ -860,3 +988,16 @@ TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second)
 
         return diffInSeconds > fifteenMins;
 }
+
+void
+TimelineView::sendRoomMessageHandler(const std::string &txn_id,
+                                     const mtx::responses::EventId &res,
+                                     mtx::http::RequestErr err)
+{
+        if (err) {
+                emit messageFailed(txn_id);
+                return;
+        }
+
+        emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string()));
+}
diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc
index b7ce53ae1..9026463d1 100644
--- a/src/timeline/TimelineViewManager.cc
+++ b/src/timeline/TimelineViewManager.cc
@@ -35,42 +35,15 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
   : QStackedWidget(parent)
 {
         setStyleSheet("border: none;");
-
-        connect(
-          http::client(), &MatrixClient::messageSent, this, &TimelineViewManager::messageSent);
-
-        connect(http::client(),
-                &MatrixClient::messageSendFailed,
-                this,
-                &TimelineViewManager::messageSendFailed);
-
-        connect(http::client(),
-                &MatrixClient::redactionCompleted,
-                this,
-                [this](const QString &room_id, const QString &event_id) {
-                        auto view = views_[room_id];
-
-                        if (view)
-                                view->removeEvent(event_id);
-                });
 }
 
 void
-TimelineViewManager::messageSent(const QString &event_id, const QString &roomid, int txn_id)
+TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
 {
-        // We save the latest valid transaction ID for later use.
-        QSettings settings;
-        settings.setValue("client/transaction_id", txn_id + 1);
+        auto view = views_[room_id];
 
-        auto view = views_[roomid];
-        view->updatePendingMessage(txn_id, event_id);
-}
-
-void
-TimelineViewManager::messageSendFailed(const QString &roomid, int txn_id)
-{
-        auto view = views_[roomid];
-        view->handleFailedMessage(txn_id);
+        if (view)
+                view->removeEvent(event_id);
 }
 
 void
diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cc
index 65ca401b1..1ad47747a 100644
--- a/src/timeline/widgets/AudioItem.cc
+++ b/src/timeline/widgets/AudioItem.cc
@@ -50,21 +50,12 @@ AudioItem::init()
         playIcon_.addFile(":/icons/icons/ui/play-sign.png");
         pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
 
-        QList<QString> url_parts = url_.toString().split("mxc://");
-        if (url_parts.size() != 2) {
-                qDebug() << "Invalid format for image" << url_.toString();
-                return;
-        }
-
-        QString media_params = url_parts[1];
-        url_                 = QString("%1/_matrix/media/r0/download/%2")
-                 .arg(http::client()->getHomeServer().toString(), media_params);
-
         player_ = new QMediaPlayer;
         player_->setMedia(QUrl(url_));
         player_->setVolume(100);
         player_->setNotifyInterval(1000);
 
+        connect(this, &AudioItem::fileDownloadedCb, this, &AudioItem::fileDownloaded);
         connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
                 if (state == QMediaPlayer::StoppedState) {
                         state_ = AudioState::Play;
@@ -129,14 +120,19 @@ AudioItem::mousePressEvent(QMouseEvent *event)
                 if (filenameToSave_.isEmpty())
                         return;
 
-                auto proxy = http::client()->downloadFile(url_);
-                connect(proxy.data(),
-                        &DownloadMediaProxy::fileDownloaded,
-                        this,
-                        [proxy, this](const QByteArray &data) {
-                                proxy->deleteLater();
-                                fileDownloaded(data);
-                        });
+                http::v2::client()->download(
+                  url_.toString().toStdString(),
+                  [this](const std::string &data,
+                         const std::string &,
+                         const std::string &,
+                         mtx::http::RequestErr err) {
+                          if (err) {
+                                  qWarning() << "failed to retrieve m.audio content:" << url_;
+                                  return;
+                          }
+
+                          emit fileDownloadedCb(QByteArray(data.data(), data.size()));
+                  });
         }
 }
 
diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cc
index f3906a04f..43689243f 100644
--- a/src/timeline/widgets/FileItem.cc
+++ b/src/timeline/widgets/FileItem.cc
@@ -49,17 +49,9 @@ FileItem::init()
 
         icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
 
-        QList<QString> url_parts = url_.toString().split("mxc://");
-        if (url_parts.size() != 2) {
-                qDebug() << "Invalid format for image" << url_.toString();
-                return;
-        }
-
-        QString media_params = url_parts[1];
-        url_                 = QString("%1/_matrix/media/r0/download/%2")
-                 .arg(http::client()->getHomeServer().toString(), media_params);
-
         setFixedHeight(Height);
+
+        connect(this, &FileItem::fileDownloadedCb, this, &FileItem::fileDownloaded);
 }
 
 FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
@@ -89,8 +81,15 @@ FileItem::openUrl()
         if (url_.toString().isEmpty())
                 return;
 
-        if (!QDesktopServices::openUrl(url_))
-                qWarning() << "Could not open url" << url_.toString();
+        auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
+        auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
+                           .arg(QString::fromStdString(http::v2::client()->server()))
+                           .arg(http::v2::client()->port())
+                           .arg(QString::fromStdString(mxc_parts.server))
+                           .arg(QString::fromStdString(mxc_parts.media_id));
+
+        if (!QDesktopServices::openUrl(urlToOpen))
+                qWarning() << "Could not open url" << urlToOpen;
 }
 
 QSize
@@ -115,14 +114,19 @@ FileItem::mousePressEvent(QMouseEvent *event)
                 if (filenameToSave_.isEmpty())
                         return;
 
-                auto proxy = http::client()->downloadFile(url_);
-                connect(proxy.data(),
-                        &DownloadMediaProxy::fileDownloaded,
-                        this,
-                        [proxy, this](const QByteArray &data) {
-                                proxy->deleteLater();
-                                fileDownloaded(data);
-                        });
+                http::v2::client()->download(
+                  url_.toString().toStdString(),
+                  [this](const std::string &data,
+                         const std::string &,
+                         const std::string &,
+                         mtx::http::RequestErr err) {
+                          if (err) {
+                                  qWarning() << "failed to retrieve m.file content:" << url_;
+                                  return;
+                          }
+
+                          emit fileDownloadedCb(QByteArray(data.data(), data.size()));
+                  });
         } else {
                 openUrl();
         }
diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cc
index 66cd31ab6..6aa010a46 100644
--- a/src/timeline/widgets/ImageItem.cc
+++ b/src/timeline/widgets/ImageItem.cc
@@ -30,37 +30,62 @@
 #include "dialogs/ImageOverlay.h"
 #include "timeline/widgets/ImageItem.h"
 
-ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
-  : QWidget(parent)
-  , event_{event}
+void
+ImageItem::downloadMedia(const QUrl &url)
 {
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
+        http::v2::client()->download(url.toString().toStdString(),
+                                     [this, url](const std::string &data,
+                                                 const std::string &,
+                                                 const std::string &,
+                                                 mtx::http::RequestErr err) {
+                                             if (err) {
+                                                     qWarning()
+                                                       << "failed to retrieve image:" << url;
+                                                     return;
+                                             }
+
+                                             QPixmap img;
+                                             img.loadFromData(QByteArray(data.data(), data.size()));
+                                             emit imageDownloaded(img);
+                                     });
+}
 
-        url_  = QString::fromStdString(event.content.url);
-        text_ = QString::fromStdString(event.content.body);
+void
+ImageItem::saveImage(const QString &filename, const QByteArray &data)
+{
+        try {
+                QFile file(filename);
 
-        QList<QString> url_parts = url_.toString().split("mxc://");
+                if (!file.open(QIODevice::WriteOnly))
+                        return;
 
-        if (url_parts.size() != 2) {
-                qDebug() << "Invalid format for image" << url_.toString();
-                return;
+                file.write(data);
+                file.close();
+        } catch (const std::exception &ex) {
+                qDebug() << "Error while saving file to:" << ex.what();
         }
+}
 
-        QString media_params = url_parts[1];
-        url_                 = QString("%1/_matrix/media/r0/download/%2")
-                 .arg(http::client()->getHomeServer().toString(), media_params);
+void
+ImageItem::init()
+{
+        setMouseTracking(true);
+        setCursor(Qt::PointingHandCursor);
+        setAttribute(Qt::WA_Hover, true);
 
-        auto proxy = http::client()->downloadImage(url_);
+        connect(this, &ImageItem::imageDownloaded, this, &ImageItem::setImage);
+        connect(this, &ImageItem::imageSaved, this, &ImageItem::saveImage);
+        downloadMedia(url_);
+}
 
-        connect(proxy.data(),
-                &DownloadMediaProxy::imageDownloaded,
-                this,
-                [this, proxy](const QPixmap &img) {
-                        proxy->deleteLater();
-                        setImage(img);
-                });
+ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
+  : QWidget(parent)
+  , event_{event}
+{
+        url_  = QString::fromStdString(event.content.url);
+        text_ = QString::fromStdString(event.content.body);
+
+        init();
 }
 
 ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
@@ -69,31 +94,7 @@ ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size,
   , text_{filename}
 {
         Q_UNUSED(size);
-
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
-
-        QList<QString> url_parts = url_.toString().split("mxc://");
-
-        if (url_parts.size() != 2) {
-                qDebug() << "Invalid format for image" << url_.toString();
-                return;
-        }
-
-        QString media_params = url_parts[1];
-        url_                 = QString("%1/_matrix/media/r0/download/%2")
-                 .arg(http::client()->getHomeServer().toString(), media_params);
-
-        auto proxy = http::client()->downloadImage(url_);
-
-        connect(proxy.data(),
-                &DownloadMediaProxy::imageDownloaded,
-                this,
-                [proxy, this](const QPixmap &img) {
-                        proxy->deleteLater();
-                        setImage(img);
-                });
+        init();
 }
 
 void
@@ -102,8 +103,15 @@ ImageItem::openUrl()
         if (url_.toString().isEmpty())
                 return;
 
-        if (!QDesktopServices::openUrl(url_))
-                qWarning() << "Could not open url" << url_.toString();
+        auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
+        auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
+                           .arg(QString::fromStdString(http::v2::client()->server()))
+                           .arg(http::v2::client()->port())
+                           .arg(QString::fromStdString(mxc_parts.server))
+                           .arg(QString::fromStdString(mxc_parts.media_id));
+
+        if (!QDesktopServices::openUrl(urlToOpen))
+                qWarning() << "Could not open url" << urlToOpen;
 }
 
 QSize
@@ -231,23 +239,17 @@ ImageItem::saveAs()
         if (filename.isEmpty())
                 return;
 
-        auto proxy = http::client()->downloadFile(url_);
-        connect(proxy.data(),
-                &DownloadMediaProxy::fileDownloaded,
-                this,
-                [proxy, filename](const QByteArray &data) {
-                        proxy->deleteLater();
-
-                        try {
-                                QFile file(filename);
-
-                                if (!file.open(QIODevice::WriteOnly))
-                                        return;
-
-                                file.write(data);
-                                file.close();
-                        } catch (const std::exception &ex) {
-                                qDebug() << "Error while saving file to:" << ex.what();
-                        }
-                });
+        http::v2::client()->download(
+          url_.toString().toStdString(),
+          [this, filename](const std::string &data,
+                           const std::string &,
+                           const std::string &,
+                           mtx::http::RequestErr err) {
+                  if (err) {
+                          qWarning() << "failed to retrieve image:" << url_;
+                          return;
+                  }
+
+                  emit imageSaved(filename, QByteArray(data.data(), data.size()));
+          });
 }
diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cc
index f5bcfd6e8..c1f68847d 100644
--- a/src/timeline/widgets/VideoItem.cc
+++ b/src/timeline/widgets/VideoItem.cc
@@ -27,15 +27,15 @@
 void
 VideoItem::init()
 {
-        QList<QString> url_parts = url_.toString().split("mxc://");
-        if (url_parts.size() != 2) {
-                qDebug() << "Invalid format for image" << url_.toString();
-                return;
-        }
+        // QList<QString> url_parts = url_.toString().split("mxc://");
+        // if (url_parts.size() != 2) {
+        //         qDebug() << "Invalid format for image" << url_.toString();
+        //         return;
+        // }
 
-        QString media_params = url_parts[1];
-        url_                 = QString("%1/_matrix/media/r0/download/%2")
-                 .arg(http::client()->getHomeServer().toString(), media_params);
+        // QString media_params = url_parts[1];
+        // url_                 = QString("%1/_matrix/media/r0/download/%2")
+        //          .arg(http::client()->getHomeServer().toString(), media_params);
 }
 
 VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)
-- 
GitLab