summary refs log tree commit diff
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-06-17 19:18:12 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-06-17 19:18:12 +0300
commit8704265978572e60f8b04d89cec2f404f5ea4113 (patch)
treef1e272705c26ce2b0121d6fffccd60f98246b84d
parentAdd Visual Studio 2017 support (#336) (diff)
parentUpdate build instructions (diff)
downloadnheko-8704265978572e60f8b04d89cec2f404f5ea4113.tar.xz
Merge branch 'e2ee'
- Support for e2ee rooms
- Implement categories & file logging
- Let the user know when the app can't reach the server (#93)

fixes #13
fixes #326
-rw-r--r--.ci/bintray-release.json37
-rwxr-xr-x.ci/install.sh7
-rwxr-xr-x.ci/linux/deploy.sh14
-rwxr-xr-x.ci/macos/deploy.sh4
-rwxr-xr-x.ci/script.sh10
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml36
-rw-r--r--CMakeLists.txt62
-rw-r--r--Makefile25
-rw-r--r--README.md50
-rw-r--r--appveyor.yml52
-rw-r--r--cmake/CompilerFlags.cmake26
-rw-r--r--cmake/MatrixStructs.cmake33
-rw-r--r--deps/CMakeLists.txt82
-rw-r--r--deps/cmake/Boost.cmake23
-rw-r--r--deps/cmake/MatrixClient.cmake31
-rw-r--r--deps/cmake/MatrixStructs.cmake25
-rw-r--r--deps/cmake/Olm.cmake34
-rw-r--r--deps/cmake/OlmCMakeLists.txt107
-rw-r--r--deps/cmake/OlmConfig.cmake.in11
-rw-r--r--deps/cmake/SpdLog.cmake22
-rw-r--r--include/AvatarProvider.h18
-rw-r--r--include/Cache.h150
-rw-r--r--include/ChatPage.h74
-rw-r--r--include/CommunitiesList.h2
-rw-r--r--include/Logging.hpp21
-rw-r--r--include/LoginPage.h33
-rw-r--r--include/MainWindow.h2
-rw-r--r--include/MatrixClient.h291
-rw-r--r--include/Olm.hpp78
-rw-r--r--include/RegisterPage.h5
-rw-r--r--include/RoomList.h2
-rw-r--r--include/TextInputWidget.h10
-rw-r--r--include/dialogs/ReCaptcha.hpp2
-rw-r--r--include/dialogs/RoomSettings.hpp24
-rw-r--r--include/timeline/TimelineItem.h15
-rw-r--r--include/timeline/TimelineView.h121
-rw-r--r--include/timeline/TimelineViewManager.h6
-rw-r--r--include/timeline/widgets/AudioItem.h7
-rw-r--r--include/timeline/widgets/FileItem.h7
-rw-r--r--include/timeline/widgets/ImageItem.h10
-rw-r--r--src/AvatarProvider.cc50
-rw-r--r--src/Cache.cc467
-rw-r--r--src/ChatPage.cc1026
-rw-r--r--src/CommunitiesList.cc64
-rw-r--r--src/Logging.cpp59
-rw-r--r--src/LoginPage.cc69
-rw-r--r--src/MainWindow.cc49
-rw-r--r--src/MatrixClient.cc1368
-rw-r--r--src/Olm.cpp228
-rw-r--r--src/RegisterPage.cc130
-rw-r--r--src/RoomList.cc54
-rw-r--r--src/TextInputWidget.cc2
-rw-r--r--src/dialogs/PreviewUploadOverlay.cc15
-rw-r--r--src/dialogs/ReCaptcha.cpp15
-rw-r--r--src/dialogs/RoomSettings.cpp185
-rw-r--r--src/main.cc57
-rw-r--r--src/timeline/TimelineItem.cc38
-rw-r--r--src/timeline/TimelineView.cc669
-rw-r--r--src/timeline/TimelineViewManager.cc53
-rw-r--r--src/timeline/widgets/AudioItem.cc39
-rw-r--r--src/timeline/widgets/FileItem.cc51
-rw-r--r--src/timeline/widgets/ImageItem.cc150
-rw-r--r--src/timeline/widgets/VideoItem.cc17
64 files changed, 3760 insertions, 2665 deletions
diff --git a/.ci/bintray-release.json b/.ci/bintray-release.json
new file mode 100644
index 00000000..7115c910
--- /dev/null
+++ b/.ci/bintray-release.json
@@ -0,0 +1,37 @@
+{
+  "files": [
+    {
+      "includePattern": "nheko-VERSION_NAME_VALUE.dmg",
+      "matrixParams": {
+        "override": 1
+      },
+      "uploadPattern": "VERSION_NAME_VALUE/nheko-VERSION_NAME_VALUE.dmg"
+    },
+    {
+      "includePattern": "nheko-VERSION_NAME_VALUE-x86_64.AppImage",
+      "matrixParams": {
+        "override": 1
+      },
+      "uploadPattern": "VERSION_NAME_VALUE/nheko-VERSION_NAME_VALUE-x86_64.AppImage"
+    }
+  ],
+  "package": {
+    "desc": "Desktop client for the Matrix protocol",
+    "issue_tracker_url": "https://github.com/mujx/nheko/issues",
+    "licenses": [
+      "GPL-3.0"
+    ],
+    "name": "nheko",
+    "public_download_numbers": true,
+    "public_stats": true,
+    "repo": "matrix",
+    "subject": "mujx",
+    "vcs_url": "https://github.com/mujx/nheko",
+    "website_url": "https://github.com/mujx/nheko"
+  },
+  "publish": true,
+  "version": {
+    "name": "VERSION_NAME_VALUE",
+    "vcs_tag": "VERSION_NAME_VALUE"
+  }
+}
diff --git a/.ci/install.sh b/.ci/install.sh
index f81f9265..ee172581 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -4,7 +4,8 @@ set -ex
 
 if [ $TRAVIS_OS_NAME == osx ]; then
     brew update
-    brew install qt5 lmdb clang-format ninja
+    brew install qt5 lmdb clang-format ninja libsodium spdlog
+    brew upgrade boost
 
     curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
     sudo python get-pip.py
@@ -23,6 +24,7 @@ if [ $TRAVIS_OS_NAME == linux ]; then
         QT_PKG="59"
     fi
 
+    sudo add-apt-repository -y ppa:chris-lea/libsodium
     sudo add-apt-repository -y ppa:beineri/opt-qt${QT_VERSION}-trusty
     sudo add-apt-repository -y ppa:george-edison55/cmake-3.x
     sudo apt-get update -qq
@@ -32,5 +34,6 @@ if [ $TRAVIS_OS_NAME == linux ]; then
         qt${QT_PKG}svg \
         qt${QT_PKG}multimedia \
         cmake \
-        liblmdb-dev
+        liblmdb-dev \
+        libsodium-dev
 fi
diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh
index 77a4fa06..7b5b8e4a 100755
--- a/.ci/linux/deploy.sh
+++ b/.ci/linux/deploy.sh
@@ -7,7 +7,7 @@ DIR=${APP}.AppDir
 TAG=`git tag -l --points-at HEAD`
 
 # Set up AppImage structure.
-mkdir -p ${DIR}/usr/{bin,share/pixmaps,share/applications}
+mkdir -p ${DIR}/usr/{bin,lib,share/pixmaps,share/applications}
 
 # Copy resources.
 cp build/nheko ${DIR}/usr/bin
@@ -30,9 +30,13 @@ unset QTDIR
 unset QT_PLUGIN_PATH 
 unset LD_LIBRARY_PATH
 
-./linuxdeployqt*.AppImage \
-    ${DIR}/usr/share/applications/nheko.desktop \
-    -bundle-non-qt-libs\
-    -appimage
+export ARCH=$(uname -m)
+
+./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -bundle-non-qt-libs
+./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -appimage
 
 chmod +x nheko-x86_64.AppImage
+
+if [ ! -z $TRAVIS_TAG ]; then
+    mv nheko-x86_64.AppImage nheko-${TRAVIS_TAG}-x86_64.AppImage
+fi
diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh
index 133c7b0e..1de95a44 100755
--- a/.ci/macos/deploy.sh
+++ b/.ci/macos/deploy.sh
@@ -15,3 +15,7 @@ mv nheko.dmg ..
 popd
 
 dmgbuild -s ./.ci/macos/settings.json "Nheko" nheko.dmg
+
+if [ ! -z $TRAVIS_TAG ]; then
+    mv nheko.dmg nheko-${TRAVIS_TAG}.dmg
+fi
diff --git a/.ci/script.sh b/.ci/script.sh
index a954eba6..377f23e1 100755
--- a/.ci/script.sh
+++ b/.ci/script.sh
@@ -10,7 +10,15 @@ if [ $TRAVIS_OS_NAME == osx ]; then
     export CMAKE_PREFIX_PATH=/usr/local/opt/qt5
 fi
 
-make ci
+# Build & install dependencies
+cmake -Hdeps -B.deps \
+    -DUSE_BUNDLED_BOOST=${USE_BUNDLED_BOOST} \
+    -DUSE_BUNDLED_SPDLOG=${USE_BUNDLED_SPDLOG}
+cmake --build .deps
+
+# Build nheko
+cmake -GNinja -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
+cmake --build build
 
 if [ $TRAVIS_OS_NAME == osx ]; then
     make lint;
diff --git a/.gitignore b/.gitignore
index 0b430f07..f5d113bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,3 +99,4 @@ package.dir
 
 # Dependencies
 .third-party
+.deps
diff --git a/.travis.yml b/.travis.yml
index 2047a2db..690f6690 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,8 @@ matrix:
           compiler: clang
           env:
               - DEPLOYMENT=1
+              - USE_BUNDLED_BOOST=0
+              - USE_BUNDLED_SPDLOG=0
         - os: linux
           compiler: gcc
           env:
@@ -22,6 +24,8 @@ matrix:
               - QT_VERSION="-5.10.1"
               - QT_PKG=510
               - DEPLOYMENT=1
+              - USE_BUNDLED_BOOST=1
+              - USE_BUNDLED_SPDLOG=1
           addons:
               apt:
                   sources: ["ubuntu-toolchain-r-test"]
@@ -33,6 +37,8 @@ matrix:
               - C_COMPILER=gcc-7
               - QT_VERSION=571
               - QT_PKG=57
+              - USE_BUNDLED_BOOST=1
+              - USE_BUNDLED_SPDLOG=1
           addons:
               apt:
                   sources: ["ubuntu-toolchain-r-test"]
@@ -44,6 +50,8 @@ matrix:
               - C_COMPILER=clang-5.0
               - QT_VERSION=592
               - QT_PKG=59
+              - USE_BUNDLED_BOOST=1
+              - USE_BUNDLED_SPDLOG=1
           addons:
               apt:
                   sources: ["ubuntu-toolchain-r-test", "llvm-toolchain-trusty-5.0"]
@@ -58,28 +66,18 @@ install:
 
 script:
     - ./.ci/script.sh
+    - sed -i -e "s/VERSION_NAME_VALUE/${TRAVIS_TAG}/g" ./.ci/bintray-release.json || true
+    - cp ./.ci/bintray-release.json .
 
 deploy:
-    - skip_cleanup: true
+    - provider: bintray
+      user: "mujx"
+      key:
+          secure: "CAVzWZPxYSOTollo9bpD4tvEbfxXjqelc32aApV48GKyJrMQljQ+mvSe25BuUtnDehxnw8affgGX23AYXmvG8P7w4hM2d7//8Lgan1zCmusV8JE432jknev6X641B4cvrywqSe0Dj3l0kS9Xgirq4BGavlI0y2vUjeJfQEv0y8GYoI72LwgyH0i82v/1Qi92Fh8429IJIb0eKmC1wGWXCmo2kd8StZRL5mSlc4TmyWI0SHpA5GrLMiQwLAuD7DjDl5mpaK2yQx+H4vBcI2SUMvmlHGgVjXikJG5gURlHbnIaaBFvO67INc1/65KtMokWuMP12zxqJiaMPtsAskOpQv4FLAYDfnigH3NxufyOIGp2cxS5RhJDQhbNsxHEDnUo1kHcO23ZYNWCuC1yUdn0RXzKhWcUsz8mKF8KJs22Ty4VjfUMZ+vqK/AbHyq4rkl8DizVRZqKF1KjSWrSv/2sT4itnHk9pmcgxAYfGuALcjrJJveI4MTwDhzXB62CKnMOqLq3sAMqvE0+BdA0BykQr7qrKtptuyP2/OFx6RDbfHQl5Klkb6cSOjxm0oUzh/8iaxgsVdCrhfE67eqkhFZ+a8lJkB/rZ4zSK1Q2Cp4nLtnxenUCW+Ptk2l7zZN6kXM1/+tcgqVROChYJ6asMUpsjFOOAVQ8SZ4TcxX1rq+pxlA="
+      skip_cleanup: true
       overwrite: true
-      provider: releases
-      api_key:
-          secure: oprXzESukFiXBeF2BXkXUlegsAQc95Ub4kc/OkoNFaYBvqpA+IGpWHmHCx5JPny/OT3Kc2Urpe2JUeGSWDHZ7UCKDjH+NzGP3uN82fHh/HiszG/Srw7+hWEHm1ve+gMK9GS8pr+yUsUrPP0UfVXlPoI4pBWa4zGi2Ndb/SqvjCgIHFLtGyoBo6CydKQ/AyWiXSMKDfJL+Dx4JLIPP4RTCBJy8ZrZ8m/a5Tgy4Ij6+djjrgYCZCEhGxSYw7xDIT/9SV8g9NkrbisqBDrILzAH8Yhe4XMRMXo88OAxV5+Vb9Rw1hrqczez6lpFDbJetv8MjofND+pSoAFwtjaL1wTFK9Ud6w4O9AuHlEQH9cGVdvsxFcosRwJVh58x51JM9ptoktqhx/HHJBTuCHCYYlHwtRwbwqnMYdLzKZG5FnujT8DG+9mcva1fL6tzW/XD505VPMWwXFC/2/pvolgAkTFFXYSALAwZlK3IgoXU8Gok/3B4iHofzQsFf6Yq3BI/88x7tVASUqiYhoKrO50+gb6pNIRCyWgGUiBEVXBp6Ziq3ORQPyQJg7i9HHUGTUu74yvGLHWLwjNQzZP/hxJZK3VlJxzyXntdOxiJc8iOzNrU+rPKBAlAaE6bQDOoniIysEgdD5BXHTLFzPvts4P1n2Ckor5/rNJ+qXR8GU+/y7e1GKU=
-      file_glob: true
-      file:
-          - nheko-x86_64.AppImage
+      file: "bintray-release.json"
       on:
-          condition: $TRAVIS_OS_NAME == linux && $DEPLOYMENT == 1
-          repo: mujx/nheko
-          tags: true
-
-    - skip_cleanup: true
-      overwrite: true
-      provider: releases
-      api_key:
-          secure: oprXzESukFiXBeF2BXkXUlegsAQc95Ub4kc/OkoNFaYBvqpA+IGpWHmHCx5JPny/OT3Kc2Urpe2JUeGSWDHZ7UCKDjH+NzGP3uN82fHh/HiszG/Srw7+hWEHm1ve+gMK9GS8pr+yUsUrPP0UfVXlPoI4pBWa4zGi2Ndb/SqvjCgIHFLtGyoBo6CydKQ/AyWiXSMKDfJL+Dx4JLIPP4RTCBJy8ZrZ8m/a5Tgy4Ij6+djjrgYCZCEhGxSYw7xDIT/9SV8g9NkrbisqBDrILzAH8Yhe4XMRMXo88OAxV5+Vb9Rw1hrqczez6lpFDbJetv8MjofND+pSoAFwtjaL1wTFK9Ud6w4O9AuHlEQH9cGVdvsxFcosRwJVh58x51JM9ptoktqhx/HHJBTuCHCYYlHwtRwbwqnMYdLzKZG5FnujT8DG+9mcva1fL6tzW/XD505VPMWwXFC/2/pvolgAkTFFXYSALAwZlK3IgoXU8Gok/3B4iHofzQsFf6Yq3BI/88x7tVASUqiYhoKrO50+gb6pNIRCyWgGUiBEVXBp6Ziq3ORQPyQJg7i9HHUGTUu74yvGLHWLwjNQzZP/hxJZK3VlJxzyXntdOxiJc8iOzNrU+rPKBAlAaE6bQDOoniIysEgdD5BXHTLFzPvts4P1n2Ckor5/rNJ+qXR8GU+/y7e1GKU=
-      file: nheko.dmg
-      on:
-          condition: $TRAVIS_OS_NAME == osx && $DEPLOYMENT == 1
+          condition: $DEPLOYMENT == 1
           repo: mujx/nheko
           tags: true
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 99e8cfe8..3b01e973 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,6 +29,15 @@ set(IDENTIFIER "com.github.mujx.nheko")
 
 add_project_meta(META_FILES_TO_INCLUDE)
 
+if(APPLE)
+    set(OPENSSL_ROOT_DIR /usr/local/opt/openssl)
+endif()
+
+if(NOT MSVC AND NOT APPLE)
+  set(THREADS_PREFER_PTHREAD_FLAG ON)
+  find_package(Threads REQUIRED)
+endif()
+
 #
 # LMDB
 #
@@ -37,7 +46,7 @@ include(LMDB)
 #
 # Discover Qt dependencies.
 #
-find_package(Qt5 COMPONENTS Core Widgets Network LinguistTools Concurrent Svg Multimedia REQUIRED)
+find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED)
 
 if (APPLE)
     find_package(Qt5MacExtras REQUIRED)
@@ -60,7 +69,20 @@ endif(NOT MSVC)
 set(CMAKE_CXX_STANDARD 14)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
-include(CompilerFlags)
+if(NOT MSVC)
+  set(
+    CMAKE_CXX_FLAGS
+    "${CMAKE_CXX_FLAGS} \
+        -Wall \
+        -Wextra \
+        -Werror \
+        -pipe \
+        -pedantic \
+        -fsized-deallocation \
+        -fdiagnostics-color=always \
+        -Wunreachable-code"
+    )
+endif()
 
 if(NOT (CMAKE_BUILD_TYPE OR CMAKE_CONFIGURATION_TYPES))
     set(CMAKE_BUILD_TYPE "Debug" CACHE STRING
@@ -151,9 +173,11 @@ set(SRC_FILES
     src/Community.cc
     src/InviteeItem.cc
     src/LoginPage.cc
+    src/Logging.cpp
     src/MainWindow.cc
     src/MatrixClient.cc
     src/QuickSwitcher.cc
+    src/Olm.cpp
     src/RegisterPage.cc
     src/RoomInfoListItem.cc
     src/RoomList.cc
@@ -175,20 +199,12 @@ set(SRC_FILES
 # ExternalProject dependencies
 set(EXTERNAL_PROJECT_DEPS "")
 
-#
-# matrix-structs
-#
-find_library(MATRIX_STRUCTS_LIBRARY 
-    NAMES matrix_structs 
-    PATHS ${MATRIX_STRUCTS_ROOT}
-          ${MATRIX_STRUCTS_ROOT}/lib
-          ${MATRIX_STRUCTS_ROOT}/lib/static)
-
-if(NOT MATRIX_STRUCTS_LIBRARY)
-    include(MatrixStructs)
-    set(EXTERNAL_PROJECT_DEPS ${EXTERNAL_PROJECT_DEPS} MatrixStructs)
-endif()
-include_directories(SYSTEM ${MATRIX_STRUCTS_INCLUDE_DIR})
+find_package(ZLIB REQUIRED)
+find_package(OpenSSL REQUIRED)
+find_package(MatrixStructs 0.1.0 REQUIRED)
+find_package(MatrixClient 0.1.0 REQUIRED)
+find_package(Olm 2 REQUIRED)
+find_package(spdlog 0.16.0 CONFIG REQUIRED)
 
 #
 # tweeny
@@ -267,7 +283,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
@@ -291,11 +306,12 @@ include(Translations)
 set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
 
 set(COMMON_LIBS
-    ${MATRIX_STRUCTS_LIBRARY} 
+    MatrixStructs::MatrixStructs
+    MatrixClient::MatrixClient
     Qt5::Widgets
-    Qt5::Network
     Qt5::Svg
-    Qt5::Concurrent)
+    Qt5::Concurrent
+    Qt5::Multimedia)
 
 if(APPVEYOR_BUILD)
     set(NHEKO_LIBS ${COMMON_LIBS} lmdb)
@@ -325,13 +341,13 @@ endif()
 
 if(APPLE)
     add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
-    target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras Qt5::Multimedia)
+    target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras)
 elseif(WIN32)
     add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS})
-    target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain Qt5::Multimedia)
+    target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain)
 else()
     add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
-    target_link_libraries (nheko ${NHEKO_LIBS} Qt5::Multimedia)
+    target_link_libraries (nheko ${NHEKO_LIBS})
 endif()
 
 if(EXTERNAL_PROJECT_DEPS)
diff --git a/Makefile b/Makefile
index 4d46e935..584aafa2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,31 @@
+DEPS_BUILD_DIR=.deps
+DEPS_SOURCE_DIR=deps
 
 debug:
-	@cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1
+	@cmake -H. -GNinja \
+		-Bbuild \
+		-DCMAKE_BUILD_TYPE=Debug \
+		-DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
+		-DCMAKE_INSTALL_PREFIX=${DEPS_BUILD_DIR}/usr
 	@cmake --build build
 
+third-party:
+	@cmake -GNinja -H${DEPS_SOURCE_DIR} -B${DEPS_BUILD_DIR} \
+		-DCMAKE_BUILD_TYPE=Release \
+		-DUSE_BUNDLED_BOOST=OFF
+	@cmake --build ${DEPS_BUILD_DIR}
+
 ci:
-	@cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
-	@cmake --build build
+	cmake -H${DEPS_SOURCE_DIR} -B${DEPS_BUILD_DIR} -DCMAKE_BUILD_TYPE=Release
+	cmake --build ${DEPS_BUILD_DIR}
+	cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
+	cmake --build build
 
 release:
-	@cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
+	@cmake -H. -GNinja \
+		-Bbuild \
+		-DCMAKE_BUILD_TYPE=Release \
+		-DCMAKE_INSTALL_PREFIX=${DEPS_BUILD_DIR}/usr
 	@cmake --build build
 
 linux-install:
diff --git a/README.md b/README.md
index fc6afab8..4812cf22 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IR
 Most of the features you would expect from a chat application are missing right now
 but we are getting close to a more feature complete client.
 Specifically there is support for:
+- E2EE encryption.
 - User registration.
 - Creating, joining & leaving rooms.
 - Sending & receiving invites.
@@ -29,20 +30,15 @@ Specifically there is support for:
 
 ## Installation
 
-### Nightly releases
-- Linux [AppImage](https://github.com/mujx/nheko/releases/download/nightly/nheko-x86_64.AppImage)
-- Windows [x64 installer](https://github.com/mujx/nheko/releases/download/nightly/nheko-installer.exe)
-- macOS [disk image](https://github.com/mujx/nheko/releases/download/nightly/nheko.dmg)
+### Releases
+
+You can find releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer) on the [Bintray repo](https://bintray.com/mujx/matrix/nheko).
 
 ### Repositories
 
 #### Arch Linux
 ```bash
-pacaur -S nheko-git
-
-# or
-
-pacaur -S nheko
+pacaur -S nheko # nheko-git
 ```
 
 #### Fedora
@@ -69,7 +65,13 @@ sudo apk add nheko
 - Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with
   Freetype, which is essential to properly support emoji.
 - CMake 3.1 or greater.
-- [LMDB](https://symas.com/lightning-memory-mapped-database/).
+- [mtxclient](https://github.com/mujx/mtxclient)
+- [matrix-structs](https://github.com/mujx/matrix-structs)
+- [LMDB](https://symas.com/lightning-memory-mapped-database/)
+- Boost 1.66 or greater.
+- [libolm](https://git.matrix.org/git/olm)
+- [libsodium](https://github.com/jedisct1/libsodium)
+- [spdlog](https://github.com/gabime/spdlog)
 - A compiler that supports C++ 14:
     - Clang 5 (tested on Travis CI)
     - GCC 7 (tested on Travis CI)
@@ -89,7 +91,16 @@ Debian as the build host in an attempt to work around this [issue](https://githu
 ##### Arch Linux
 
 ```bash
-sudo pacman -S qt5-base qt5-tools qt5-multimedia qt5-svg cmake gcc fontconfig lmdb
+sudo pacman -S qt5-base \
+    qt5-tools \
+    qt5-multimedia \
+    qt5-svg \
+    cmake \
+    gcc \
+    fontconfig \
+    lmdb \
+    boost \
+    libsodium
 ```
 
 ##### Gentoo Linux
@@ -105,14 +116,14 @@ sudo add-apt-repository ppa:beineri/opt-qt592-trusty
 sudo add-apt-repository ppa:george-edison55/cmake-3.x
 sudo add-apt-repository ppa:ubuntu-toolchain-r-test
 sudo apt-get update
-sudo apt-get install -y g++-7 qt59base qt59svg qt59tools qt59multimedia cmake liblmdb-dev
+sudo apt-get install -y g++-7 qt59base qt59svg qt59tools qt59multimedia cmake liblmdb-dev libsodium-dev 
 ```
 
 ##### macOS (Xcode 8 or later)
 
 ```bash
 brew update
-brew install qt5 lmdb cmake llvm
+brew install qt5 lmdb cmake llvm libsodium spdlog boost
 ```
 
 ##### Windows
@@ -136,16 +147,19 @@ cd vcpkg
 
 ### Building
 
-Clone the repo and run
+First we need to install the rest of the dependencies that are not available in our system
 
 ```bash
-make release
+cmake -Hdeps -B.deps \
+    -DUSE_BUNDLED_BOOST=OFF # if we already have boost & spdlog installed.
+    -DUSE_BUNDLED_SPDLOG=OFF
+cmake --build .deps
 ```
 
-which invokes cmake and translates to
+We can now build nheko by pointing it to the path that we installed the dependencies.
 
 ```bash
-cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
+cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=.deps/usr
 cmake --build build
 ```
 
@@ -162,7 +176,7 @@ You might need to pass `-DCMAKE_PREFIX_PATH` to cmake to point it at your qt5 in
 e.g on macOS
 
 ```
-cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_PREFIX_PATH=$(brew --prefix qt5)
+cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt5)
 cmake --build build
 ```
 
diff --git a/appveyor.yml b/appveyor.yml
index adbad603..48c7e5cc 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -14,8 +14,20 @@ build:
 install:
     - set QT_DIR=C:\Qt\5.10.1\msvc2017_64
     - set PATH=%PATH%;%QT_DIR%\bin;C:\MinGW\bin
+    - set PATH=%PATH%;C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\bin
     - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat"
-    - vcpkg install lmdb:%PLATFORM%-windows
+    - vcpkg install
+            boost-asio:%PLATFORM%-windows
+            boost-beast:%PLATFORM%-windows
+            boost-iostreams:%PLATFORM%-windows
+            boost-random:%PLATFORM%-windows
+            boost-signals2:%PLATFORM%-windows
+            boost-system:%PLATFORM%-windows
+            boost-thread:%PLATFORM%-windows
+            libsodium:%PLATFORM%-windows
+            lmdb:%PLATFORM%-windows
+            openssl:%PLATFORM%-windows
+            zlib:%PLATFORM%-windows
 
 build_script:
     # VERSION format:     branch-master/branch-1.2
@@ -35,9 +47,16 @@ build_script:
     - echo %VERSION%
     - echo %INSTVERSION%
     - echo %DATE%
+
+    # Build & install the dependencies
+    - cmake -G "Visual Studio 15 2017 Win64" -Hdeps -B.deps
+        -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake
+        -DUSE_BUNDLED_BOOST=OFF
+    - cmake --build .deps --config Release
+
+    # Build nheko
     - cmake -G "Visual Studio 15 2017 Win64" -H. -Bbuild
       -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake
-      -DCMAKE_BUILD_TYPE=Release
     - cmake --build build --config Release
 
 after_build:
@@ -48,12 +67,9 @@ after_build:
     - copy build\Release\nheko.exe NhekoRelease\nheko.exe
     - windeployqt --qmldir %QT_DIR%\qml\ --release NhekoRelease\nheko.exe
 
-    - copy C:\Tools\vcpkg\installed\x64-windows\lib\lmdb.lib .\NhekoRelease\lmdb.lib
-    - copy C:\Tools\vcpkg\installed\x64-windows\bin\lmdb.dll .\NhekoRelease\lmdb.dll
+    - copy C:\Tools\vcpkg\installed\x64-windows\lib\*.lib .\NhekoRelease\
+    - copy C:\Tools\vcpkg\installed\x64-windows\bin\*.dll .\NhekoRelease\
 
-    - copy C:\OpenSSL-Win64\bin\ssleay32.dll .\NhekoRelease\ssleay32.dll
-    - copy C:\OpenSSL-Win64\bin\libeay32.dll .\NhekoRelease\libeay32.dll
-    - copy C:\OpenSSL-Win64\lib\libeay32.lib .\NhekoRelease\libeay32.lib
     - 7z a nheko_win_64.zip .\NhekoRelease\*
     - ls -lh build\Release\
     - ls -lh NhekoRelease\
@@ -96,17 +112,23 @@ after_build:
     - set PATH=%BUILD%\tools\bin;%PATH%
     - binarycreator.exe -f -c installer\config\config.xml -p installer\packages nheko-installer.exe
 
+    - mv nheko-installer.exe nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe
+
 deploy:
-    description: "Development builds"
-    provider: GitHub
-    auth_token:
-        secure: YqB7hcM+4482eSHhtVR7ZA7N7lE78y8BC897/7UDTBQd+NWdWFW/6S+oKDie9TT7
-    artifact: nheko-installer.exe
-    force_update: true
-    prerelease: true
+    provider: BinTray
+    username: mujx
+    api_key:
+        secure: "hhhAH6csIrPEVH92NNQkiGCkuON6l6sfhbZk+pvzDAM3vHex7YbqFKW6v5UjAS8v"
+    subject: mujx
+    repo: matrix
+    package: nheko
+    version: $(APPVEYOR_REPO_TAG_NAME)
+    publish: true
+    override: true
+    artifact: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe
     on:
         appveyor_repo_tag: true
 
 artifacts:
     - path: nheko_win_64.zip
-    - path: nheko-installer.exe
+    - path: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe
diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake
deleted file mode 100644
index f5860a99..00000000
--- a/cmake/CompilerFlags.cmake
+++ /dev/null
@@ -1,26 +0,0 @@
-if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU")
-    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \
-        -Wall \
-        -Wextra \
-        -Werror \
-        -pipe \
-        -Wno-unused-function \
-        -pedantic \
-        -Wunreachable-code")
-
-    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
-        execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION)
-
-        if (GCC_VERSION VERSION_GREATER 4.9)
-            set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always" )
-        endif()
-    endif()
-
-    if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
-        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always" )
-    endif()
-endif()
-
-if(NOT APPLE AND NOT MSVC)
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
-endif()
diff --git a/cmake/MatrixStructs.cmake b/cmake/MatrixStructs.cmake
deleted file mode 100644
index af694c0f..00000000
--- a/cmake/MatrixStructs.cmake
+++ /dev/null
@@ -1,33 +0,0 @@
-include(ExternalProject)
-
-#
-# Build matrix-structs.
-#
-
-set(THIRD_PARTY_ROOT ${CMAKE_SOURCE_DIR}/.third-party)
-set(MATRIX_STRUCTS_ROOT ${THIRD_PARTY_ROOT}/matrix_structs)
-set(MATRIX_STRUCTS_INCLUDE_DIR ${MATRIX_STRUCTS_ROOT}/include)
-set(MATRIX_STRUCTS_LIBRARY matrix_structs)
-
-link_directories(${MATRIX_STRUCTS_ROOT})
-
-set(WINDOWS_FLAGS "")
-
-if(MSVC)
-    set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64")
-endif()
-
-ExternalProject_Add(
-  MatrixStructs
-
-  GIT_REPOSITORY https://github.com/mujx/matrix-structs
-  GIT_TAG 5e57c2385a79b6629d1998fec4a7c0baee23555e
-
-  BUILD_IN_SOURCE 1
-  SOURCE_DIR ${MATRIX_STRUCTS_ROOT}
-  CONFIGURE_COMMAND ${CMAKE_COMMAND}
-    -DCMAKE_BUILD_TYPE=Release ${MATRIX_STRUCTS_ROOT}
-    ${WINDOWS_FLAGS}
-  BUILD_COMMAND ${CMAKE_COMMAND} --build ${MATRIX_STRUCTS_ROOT} --config Release
-  INSTALL_COMMAND ""
-)
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
new file mode 100644
index 00000000..06b1f7e1
--- /dev/null
+++ b/deps/CMakeLists.txt
@@ -0,0 +1,82 @@
+cmake_minimum_required(VERSION 3.1)
+project(NHEKO_DEPS)
+
+# Point CMake at any custom modules we may ship
+list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
+
+if(NOT CMAKE_BUILD_TYPE)
+  set(CMAKE_BUILD_TYPE Release)
+endif()
+
+set(DEPS_INSTALL_DIR "${CMAKE_BINARY_DIR}/usr"
+    CACHE PATH "Dependencies install directory.")
+set(DEPS_BIN_DIR "${DEPS_INSTALL_DIR}/bin"
+    CACHE PATH "Dependencies binary install directory.")
+set(DEPS_LIB_DIR "${DEPS_INSTALL_DIR}/lib"
+    CACHE PATH "Dependencies library install directory.")
+set(DEPS_BUILD_DIR "${CMAKE_BINARY_DIR}/build"
+    CACHE PATH "Dependencies build directory.")
+set(DEPS_DOWNLOAD_DIR "${DEPS_BUILD_DIR}/downloads"
+    CACHE PATH "Dependencies download directory.")
+
+option(USE_BUNDLED "Use bundled dependencies." ON)
+
+option(USE_BUNDLED_BOOST "Use the bundled version of Boost." ${USE_BUNDLED})
+option(USE_BUNDLED_SPDLOG "Use the bundled version of spdlog." ${USE_BUNDLED})
+option(USE_BUNDLED_OLM "Use the bundled version of libolm." ${USE_BUNDLED})
+option(USE_BUNDLED_MATRIX_STRUCTS "Use the bundled version of matrix-structs."
+       ${USE_BUNDLED})
+option(USE_BUNDLED_MATRIX_CLIENT "Use the bundled version of mtxclient."
+       ${USE_BUNDLED})
+
+include(ExternalProject)
+
+set(BOOST_URL
+    https://dl.bintray.com/boostorg/release/1.66.0/source/boost_1_66_0.tar.bz2)
+set(BOOST_SHA256
+    5721818253e6a0989583192f96782c4a98eb6204965316df9f5ad75819225ca9)
+
+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 68188721e042ff5b47ea9a87aa97d3a9efbca989)
+
+set(OLM_URL https://git.matrix.org/git/olm.git)
+set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae)
+
+set(SPDLOG_URL https://github.com/gabime/spdlog)
+set(SPDLOG_TAG 560df2878ad308b27873b3cc5e810635d69cfad6)
+
+if(USE_BUNDLED_BOOST)
+  include(Boost)
+endif()
+
+if(USE_BUNDLED_SPDLOG)
+  include(SpdLog)
+endif()
+
+if(USE_BUNDLED_OLM)
+  include(Olm)
+endif()
+
+if(USE_BUNDLED_MATRIX_STRUCTS)
+  include(MatrixStructs)
+endif()
+
+if(WIN32)
+  if("${TARGET_ARCH}" STREQUAL "X86_64")
+    set(TARGET_ARCH x64)
+  elseif(TARGET_ARCH STREQUAL "X86")
+    set(TARGET_ARCH ia32)
+  endif()
+endif()
+
+add_custom_target(third-party ALL
+                  COMMAND ${CMAKE_COMMAND} -E touch .third-party
+                  DEPENDS ${THIRD_PARTY_DEPS})
+
+if(USE_BUNDLED_MATRIX_CLIENT)
+  include(MatrixClient)
+  add_dependencies(MatrixClient third-party)
+endif()
diff --git a/deps/cmake/Boost.cmake b/deps/cmake/Boost.cmake
new file mode 100644
index 00000000..572d1d07
--- /dev/null
+++ b/deps/cmake/Boost.cmake
@@ -0,0 +1,23 @@
+if(WIN32)
+  message(STATUS "Building Boost in Windows is not supported (skipping)")
+  return()
+endif()
+
+ExternalProject_Add(
+  Boost
+
+  URL ${BOOST_URL}
+  URL_HASH SHA256=${BOOST_SHA256}
+  DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/boost
+  DOWNLOAD_NO_PROGRESS 0
+
+  BUILD_IN_SOURCE 1
+  SOURCE_DIR ${DEPS_BUILD_DIR}/boost
+  CONFIGURE_COMMAND ${DEPS_BUILD_DIR}/boost/bootstrap.sh
+    --with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex
+    --prefix=${DEPS_INSTALL_DIR}
+  BUILD_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 variant=release link=static threading=multi --layout=system
+  INSTALL_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 install
+)
+
+list(APPEND THIRD_PARTY_DEPS Boost)
diff --git a/deps/cmake/MatrixClient.cmake b/deps/cmake/MatrixClient.cmake
new file mode 100644
index 00000000..d8dd48c7
--- /dev/null
+++ b/deps/cmake/MatrixClient.cmake
@@ -0,0 +1,31 @@
+set(PLATFORM_FLAGS "")
+
+if(MSVC)
+    set(PLATFORM_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64")
+endif()
+
+if(APPLE)
+    set(PLATFORM_FLAGS "-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl")
+endif()
+
+ExternalProject_Add(
+  MatrixClient
+
+  DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/mtxclient
+  GIT_REPOSITORY ${MTXCLIENT_URL}
+  GIT_TAG ${MTXCLIENT_TAG}
+
+  BUILD_IN_SOURCE 1
+  SOURCE_DIR ${DEPS_BUILD_DIR}/mtxclient
+  CONFIGURE_COMMAND ${CMAKE_COMMAND}
+        -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+        -DCMAKE_BUILD_TYPE=Release 
+        -DBUILD_LIB_TESTS=OFF
+        -DBUILD_LIB_EXAMPLES=OFF
+        -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
+        ${PLATFORM_FLAGS}
+        ${DEPS_BUILD_DIR}/mtxclient
+  BUILD_COMMAND 
+        ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/mtxclient --config Release)
+
+list(APPEND THIRD_PARTY_DEPS MatrixClient)
diff --git a/deps/cmake/MatrixStructs.cmake b/deps/cmake/MatrixStructs.cmake
new file mode 100644
index 00000000..fd12ad39
--- /dev/null
+++ b/deps/cmake/MatrixStructs.cmake
@@ -0,0 +1,25 @@
+set(WINDOWS_FLAGS "")
+
+if(MSVC)
+    set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64")
+endif()
+
+ExternalProject_Add(
+  MatrixStructs
+
+  DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/matrix_structs
+  GIT_REPOSITORY ${MATRIX_STRUCTS_URL}
+  GIT_TAG ${MATRIX_STRUCTS_TAG}
+
+  BUILD_IN_SOURCE 1
+  SOURCE_DIR ${DEPS_BUILD_DIR}/matrix_structs
+  CONFIGURE_COMMAND ${CMAKE_COMMAND} 
+        -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+        -DCMAKE_BUILD_TYPE=Release 
+        ${DEPS_BUILD_DIR}/matrix_structs
+        ${WINDOWS_FLAGS}
+  BUILD_COMMAND ${CMAKE_COMMAND} 
+        --build ${DEPS_BUILD_DIR}/matrix_structs 
+        --config Release)
+
+list(APPEND THIRD_PARTY_DEPS MatrixStructs)
diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake
new file mode 100644
index 00000000..a5b8be76
--- /dev/null
+++ b/deps/cmake/Olm.cmake
@@ -0,0 +1,34 @@
+set(WINDOWS_FLAGS "")
+
+if(MSVC)
+    set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64")
+endif()
+
+ExternalProject_Add(
+  Olm
+
+  GIT_REPOSITORY ${OLM_URL}
+  GIT_TAG ${OLM_TAG}
+
+  BUILD_IN_SOURCE 1
+  SOURCE_DIR ${DEPS_BUILD_DIR}/olm
+  CONFIGURE_COMMAND ${CMAKE_COMMAND} -E copy
+      ${CMAKE_CURRENT_SOURCE_DIR}/cmake/OlmCMakeLists.txt
+      ${DEPS_BUILD_DIR}/olm/CMakeLists.txt
+    COMMAND ${CMAKE_COMMAND} -E copy
+      ${CMAKE_CURRENT_SOURCE_DIR}/cmake/OlmConfig.cmake.in
+      ${DEPS_BUILD_DIR}/olm/cmake/OlmConfig.cmake.in
+    COMMAND ${CMAKE_COMMAND}
+      -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+      -DCMAKE_BUILD_TYPE=Release
+      ${DEPS_BUILD_DIR}/olm
+      ${WINDOWS_FLAGS}
+  BUILD_COMMAND ${CMAKE_COMMAND}
+    --build ${DEPS_BUILD_DIR}/olm
+    --config Release
+  INSTALL_COMMAND ${CMAKE_COMMAND}
+    --build ${DEPS_BUILD_DIR}/olm
+    --config Release
+    --target install)
+
+list(APPEND THIRD_PARTY_DEPS Olm)
diff --git a/deps/cmake/OlmCMakeLists.txt b/deps/cmake/OlmCMakeLists.txt
new file mode 100644
index 00000000..529cbb92
--- /dev/null
+++ b/deps/cmake/OlmCMakeLists.txt
@@ -0,0 +1,107 @@
+cmake_minimum_required(VERSION 3.1)
+
+project(olm VERSION 2.2.2 LANGUAGES CXX C)
+
+add_definitions(-DOLMLIB_VERSION_MAJOR=${PROJECT_VERSION_MAJOR})
+add_definitions(-DOLMLIB_VERSION_MINOR=${PROJECT_VERSION_MINOR})
+add_definitions(-DOLMLIB_VERSION_PATCH=${PROJECT_VERSION_PATCH})
+
+set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+
+if(NOT CMAKE_BUILD_TYPE)
+    set(CMAKE_BUILD_TYPE Release)
+endif()
+
+add_library(olm
+    src/account.cpp
+    src/base64.cpp
+    src/cipher.cpp
+    src/crypto.cpp
+    src/memory.cpp
+    src/message.cpp
+    src/pickle.cpp
+    src/ratchet.cpp
+    src/session.cpp
+    src/utility.cpp
+
+    src/ed25519.c
+    src/error.c
+    src/inbound_group_session.c
+    src/megolm.c
+    src/olm.cpp
+    src/outbound_group_session.c
+    src/pickle_encoding.c
+
+    lib/crypto-algorithms/aes.c
+    lib/crypto-algorithms/sha256.c
+    lib/curve25519-donna/curve25519-donna.c)
+add_library(Olm::Olm ALIAS olm)
+
+target_include_directories(olm
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
+        $<INSTALL_INTERFACE:include>
+    PRIVATE
+        ${CMAKE_CURRENT_SOURCE_DIR}/lib)
+
+set_target_properties(olm PROPERTIES
+   SOVERSION ${PROJECT_VERSION_MAJOR}
+   VERSION ${PROJECT_VERSION})
+
+set_target_properties(olm PROPERTIES
+    ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}
+    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}
+    RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
+
+#
+# Installation
+#
+include(GNUInstallDirs)
+set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/Olm)
+install(TARGETS olm
+    EXPORT olm-targets
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
+
+# The exported target will be named Olm.
+set_target_properties(olm PROPERTIES EXPORT_NAME Olm)
+install(FILES
+    ${CMAKE_SOURCE_DIR}/include/olm/olm.h
+    ${CMAKE_SOURCE_DIR}/include/olm/outbound_group_session.h
+    ${CMAKE_SOURCE_DIR}/include/olm/inbound_group_session.h
+    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/olm)
+
+# Export the targets to a script.
+install(EXPORT olm-targets
+  FILE OlmTargets.cmake
+  NAMESPACE Olm::
+  DESTINATION ${INSTALL_CONFIGDIR})
+
+# Create a ConfigVersion.cmake file.
+include(CMakePackageConfigHelpers)
+write_basic_package_version_file(
+    ${CMAKE_CURRENT_BINARY_DIR}/OlmConfigVersion.cmake
+    VERSION ${PROJECT_VERSION}
+    COMPATIBILITY SameMajorVersion)
+
+configure_package_config_file(
+    ${CMAKE_CURRENT_LIST_DIR}/cmake/OlmConfig.cmake.in
+    ${CMAKE_CURRENT_BINARY_DIR}/OlmConfig.cmake
+    INSTALL_DESTINATION ${INSTALL_CONFIGDIR})
+
+#Install the config & configversion.
+install(FILES
+    ${CMAKE_CURRENT_BINARY_DIR}/OlmConfig.cmake
+    ${CMAKE_CURRENT_BINARY_DIR}/OlmConfigVersion.cmake
+    DESTINATION ${INSTALL_CONFIGDIR})
+
+# Register package in user's package registry
+export(EXPORT olm-targets
+    FILE ${CMAKE_CURRENT_BINARY_DIR}/OlmTargets.cmake
+    NAMESPACE Olm::)
+export(PACKAGE Olm)
diff --git a/deps/cmake/OlmConfig.cmake.in b/deps/cmake/OlmConfig.cmake.in
new file mode 100644
index 00000000..b670fe85
--- /dev/null
+++ b/deps/cmake/OlmConfig.cmake.in
@@ -0,0 +1,11 @@
+get_filename_component(Olm_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
+include(CMakeFindDependencyMacro)
+
+list(APPEND CMAKE_MODULE_PATH ${Olm_CMAKE_DIR})
+list(REMOVE_AT CMAKE_MODULE_PATH -1)
+
+if(NOT TARGET Olm::Olm)
+  include("${Olm_CMAKE_DIR}/OlmTargets.cmake")
+endif()
+
+set(Olm_LIBRARIES Olm::Olm)
diff --git a/deps/cmake/SpdLog.cmake b/deps/cmake/SpdLog.cmake
new file mode 100644
index 00000000..5a5f318c
--- /dev/null
+++ b/deps/cmake/SpdLog.cmake
@@ -0,0 +1,22 @@
+set(WINDOWS_FLAGS "")
+
+if(MSVC)
+    set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64")
+endif()
+
+ExternalProject_Add(
+  SpdLog
+
+  GIT_REPOSITORY ${SPDLOG_URL}
+  GIT_TAG ${SPDLOG_TAG}
+
+  BUILD_IN_SOURCE 1
+  SOURCE_DIR ${DEPS_BUILD_DIR}/spdlog
+  CONFIGURE_COMMAND ${CMAKE_COMMAND}
+        -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+        -DSPDLOG_BUILD_EXAMPLES=0
+        -DSPDLOG_BUILD_TESTING=0
+        ${DEPS_BUILD_DIR}/spdlog
+        ${WINDOWS_FLAGS})
+
+list(APPEND THIRD_PARTY_DEPS SpdLog)
diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h
index ce82f2aa..4b4e15e9 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 d2574b76..f5a655cf 100644
--- a/include/Cache.h
+++ b/include/Cache.h
@@ -17,13 +17,18 @@
 
 #pragma once
 
-#include <QDebug>
+#include <boost/optional.hpp>
+
 #include <QDir>
 #include <QImage>
+
 #include <json.hpp>
 #include <lmdb++.h>
 #include <mtx/events/join_rules.hpp>
 #include <mtx/responses.hpp>
+#include <mtxclient/crypto/client.hpp>
+#include <mutex>
+
 using mtx::events::state::JoinRule;
 
 struct RoomMember
@@ -140,6 +145,82 @@ struct RoomSearchResult
 Q_DECLARE_METATYPE(RoomSearchResult)
 Q_DECLARE_METATYPE(RoomInfo)
 
+// Extra information associated with an outbound megolm session.
+struct OutboundGroupSessionData
+{
+        std::string session_id;
+        std::string session_key;
+        uint64_t message_index = 0;
+};
+
+inline void
+to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg)
+{
+        obj["session_id"]    = msg.session_id;
+        obj["session_key"]   = msg.session_key;
+        obj["message_index"] = msg.message_index;
+}
+
+inline void
+from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg)
+{
+        msg.session_id    = obj.at("session_id");
+        msg.session_key   = obj.at("session_key");
+        msg.message_index = obj.at("message_index");
+}
+
+struct OutboundGroupSessionDataRef
+{
+        OlmOutboundGroupSession *session;
+        OutboundGroupSessionData data;
+};
+
+struct DevicePublicKeys
+{
+        std::string ed25519;
+        std::string curve25519;
+};
+
+inline void
+to_json(nlohmann::json &obj, const DevicePublicKeys &msg)
+{
+        obj["ed25519"]    = msg.ed25519;
+        obj["curve25519"] = msg.curve25519;
+}
+
+inline void
+from_json(const nlohmann::json &obj, DevicePublicKeys &msg)
+{
+        msg.ed25519    = obj.at("ed25519");
+        msg.curve25519 = obj.at("curve25519");
+}
+
+//! Represents a unique megolm session identifier.
+struct MegolmSessionIndex
+{
+        //! The room in which this session exists.
+        std::string room_id;
+        //! The session_id of the megolm session.
+        std::string session_id;
+        //! The curve25519 public key of the sender.
+        std::string sender_key;
+
+        //! Representation to be used in a hash map.
+        std::string to_hash() const { return room_id + session_id + sender_key; }
+};
+
+struct OlmSessionStorage
+{
+        // Megolm sessions
+        std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions;
+        std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions;
+        std::map<std::string, OutboundGroupSessionData> group_outbound_session_data;
+
+        // Guards for accessing megolm sessions.
+        std::mutex group_outbound_mtx;
+        std::mutex group_inbound_mtx;
+};
+
 class Cache : public QObject
 {
         Q_OBJECT
@@ -192,7 +273,7 @@ public:
         void saveState(const mtx::responses::Sync &res);
         bool isInitialized() const;
 
-        QString nextBatchToken() const;
+        std::string nextBatchToken() const;
 
         void deleteData();
 
@@ -206,6 +287,9 @@ public:
         bool isFormatValid();
         void setCurrentFormat();
 
+        //! Retrieve all the user ids from a room.
+        std::vector<std::string> roomMembers(const std::string &room_id);
+
         //! Check if the given user has power leve greater than than
         //! lowest power level of the given events.
         bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
@@ -237,6 +321,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);
@@ -259,6 +344,51 @@ public:
         //! Check if we have sent a desktop notification for the given event id.
         bool isNotificationSent(const std::string &event_id);
 
+        //! Mark a room that uses e2e encryption.
+        void setEncryptedRoom(const std::string &room_id);
+        bool isRoomEncrypted(const std::string &room_id);
+
+        //! Save the public keys for a device.
+        void saveDeviceKeys(const std::string &device_id);
+        void getDeviceKeys(const std::string &device_id);
+
+        //! Save the device list for a user.
+        void setDeviceList(const std::string &user_id, const std::vector<std::string> &devices);
+        std::vector<std::string> getDeviceList(const std::string &user_id);
+
+        //
+        // Outbound Megolm Sessions
+        //
+        void saveOutboundMegolmSession(const std::string &room_id,
+                                       const OutboundGroupSessionData &data,
+                                       mtx::crypto::OutboundGroupSessionPtr session);
+        OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
+        bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
+        void updateOutboundMegolmSession(const std::string &room_id, int message_index);
+
+        //
+        // Inbound Megolm Sessions
+        //
+        void saveInboundMegolmSession(const MegolmSessionIndex &index,
+                                      mtx::crypto::InboundGroupSessionPtr session);
+        OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index);
+        bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept;
+
+        //
+        // Olm Sessions
+        //
+        void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session);
+        std::vector<std::string> getOlmSessions(const std::string &curve25519);
+        boost::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519,
+                                                                  const std::string &session_id);
+
+        void saveOlmAccount(const std::string &pickled);
+        std::string restoreOlmAccount();
+
+        void restoreSessions();
+
+        OlmSessionStorage session_storage;
+
 private:
         //! Save an invited room.
         void saveInvite(lmdb::txn &txn,
@@ -431,6 +561,16 @@ private:
                 return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
         }
 
+        //! Retrieves or creates the database that stores the open OLM sessions between our device
+        //! and the given curve25519 key which represents another device.
+        //!
+        //! Each entry is a map from the session_id to the pickled representation of the session.
+        lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key)
+        {
+                return lmdb::dbi::open(
+                  txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE);
+        }
+
         QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event)
         {
                 if (!event.content.display_name.empty())
@@ -450,6 +590,12 @@ private:
         lmdb::dbi readReceiptsDb_;
         lmdb::dbi notificationsDb_;
 
+        lmdb::dbi devicesDb_;
+        lmdb::dbi deviceKeysDb_;
+
+        lmdb::dbi inboundMegolmSessionDb_;
+        lmdb::dbi outboundMegolmSessionDb_;
+
         QString localUserId_;
         QString cacheDirectory_;
 };
diff --git a/include/ChatPage.h b/include/ChatPage.h
index b6c431e4..ffea2914 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -17,6 +17,8 @@
 
 #pragma once
 
+#include <atomic>
+
 #include <QFrame>
 #include <QHBoxLayout>
 #include <QMap>
@@ -27,8 +29,7 @@
 #include "Cache.h"
 #include "CommunitiesList.h"
 #include "Community.h"
-
-#include <mtx.hpp>
+#include "MatrixClient.h"
 
 class OverlayModal;
 class QuickSwitcher;
@@ -50,9 +51,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 +69,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 +110,50 @@ 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 tryDelayedSyncCb();
+        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_;
 
+        //! Handler callback for initial sync. It doesn't run on the main thread so all
+        //! communication with the GUI should be done through signals.
+        void initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err);
+        void tryInitialSync();
+        void trySync();
+        void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
+        void getProfileInfo();
+
         //! Check if the given room is currently open.
         bool isRoomActive(const QString &room_id)
         {
@@ -161,8 +209,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 3299e7c4..78b9602e 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 00000000..2feae60d
--- /dev/null
+++ b/include/Logging.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <memory>
+#include <spdlog/spdlog.h>
+
+namespace nhlog {
+void
+init(const std::string &file);
+
+std::shared_ptr<spdlog::logger>
+ui();
+
+std::shared_ptr<spdlog::logger>
+net();
+
+std::shared_ptr<spdlog::logger>
+db();
+
+std::shared_ptr<spdlog::logger>
+crypto();
+}
diff --git a/include/LoginPage.h b/include/LoginPage.h
index 34a08df9..c52ccaa4 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 0fbc7567..b068e8f6 100644
--- a/include/MainWindow.h
+++ b/include/MainWindow.h
@@ -96,7 +96,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 eae57281..7ea5e0b7 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -1,287 +1,28 @@
-/*
- * 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)
+Q_DECLARE_METATYPE(std::string)
+Q_DECLARE_METATYPE(std::vector<std::string>)
 
-/*
- * 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_;
+namespace http {
+namespace v2 {
+mtx::http::Client *
+client();
 
-        //! 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_;
-};
+bool
+is_logged_in();
+}
 
-namespace http {
 //! Initialize the http module
 void
 init();
-
-//! Retrieve the client instance.
-MatrixClient *
-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;
 }
diff --git a/include/Olm.hpp b/include/Olm.hpp
new file mode 100644
index 00000000..6f871628
--- /dev/null
+++ b/include/Olm.hpp
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <boost/optional.hpp>
+
+#include <memory>
+#include <mtx.hpp>
+#include <mtxclient/crypto/client.hpp>
+
+constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
+
+namespace olm {
+
+struct OlmCipherContent
+{
+        std::string body;
+        uint8_t type;
+};
+
+inline void
+from_json(const nlohmann::json &obj, OlmCipherContent &msg)
+{
+        msg.body = obj.at("body");
+        msg.type = obj.at("type");
+}
+
+struct OlmMessage
+{
+        std::string sender_key;
+        std::string sender;
+
+        using RecipientKey = std::string;
+        std::map<RecipientKey, OlmCipherContent> ciphertext;
+};
+
+inline void
+from_json(const nlohmann::json &obj, OlmMessage &msg)
+{
+        if (obj.at("type") != "m.room.encrypted")
+                throw std::invalid_argument("invalid type for olm message");
+
+        if (obj.at("content").at("algorithm") != OLM_ALGO)
+                throw std::invalid_argument("invalid algorithm for olm message");
+
+        msg.sender     = obj.at("sender");
+        msg.sender_key = obj.at("content").at("sender_key");
+        msg.ciphertext =
+          obj.at("content").at("ciphertext").get<std::map<std::string, OlmCipherContent>>();
+}
+
+mtx::crypto::OlmClient *
+client();
+
+void
+handle_to_device_messages(const std::vector<nlohmann::json> &msgs);
+
+boost::optional<json>
+try_olm_decryption(const std::string &sender_key, const OlmCipherContent &content);
+
+void
+handle_olm_message(const OlmMessage &msg);
+
+//! Establish a new inbound megolm session with the decrypted payload from olm.
+void
+create_inbound_megolm_session(const std::string &sender,
+                              const std::string &sender_key,
+                              const nlohmann::json &payload);
+
+void
+handle_pre_key_olm_message(const std::string &sender,
+                           const std::string &sender_key,
+                           const OlmCipherContent &content);
+
+mtx::events::msg::Encrypted
+encrypt_group_message(const std::string &room_id,
+                      const std::string &device_id,
+                      const std::string &body);
+
+} // namespace olm
diff --git a/include/RegisterPage.h b/include/RegisterPage.h
index f4d97816..d02de7c4 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 98d9443e..59b0e865 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 c679b9b2..af58c2c3 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 1eda40c7..5f47b0eb 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 375a531e..6cab03b7 100644
--- a/include/dialogs/RoomSettings.hpp
+++ b/include/dialogs/RoomSettings.hpp
@@ -5,16 +5,17 @@
 
 #include "Cache.h"
 
+class Avatar;
 class FlatButton;
-class TextField;
+class QComboBox;
 class QHBoxLayout;
-class Avatar;
-class QPixmap;
-class QLayout;
 class QLabel;
-class QComboBox;
-class TextField;
 class QLabel;
+class QLayout;
+class QPixmap;
+class TextField;
+class TextField;
+class Toggle;
 
 template<class T>
 class QSharedPointer;
@@ -30,6 +31,9 @@ public:
 
 signals:
         void nameChanged(const QString &roomName);
+        void nameEventSentCb(const QString &newName);
+        void topicEventSentCb();
+        void stateEventErrorCb(const QString &msg);
 
 private:
         QString roomId_;
@@ -81,6 +85,7 @@ public:
 
 signals:
         void closing();
+        void enableEncryptionError(const QString &msg);
 
 protected:
         void paintEvent(QPaintEvent *event) override;
@@ -95,13 +100,15 @@ private:
         void setupEditButton();
         //! Retrieve the current room information from cache.
         void retrieveRoomInfo();
+        void enableEncryption();
 
         //! Whether the user would be able to change the name or the topic of the room.
-        bool hasEditRights_ = true;
+        bool hasEditRights_  = true;
+        bool usesEncryption_ = false;
         QHBoxLayout *editLayout_;
 
         // Button section
-        FlatButton *saveBtn_;
+        FlatButton *okBtn_;
         FlatButton *cancelBtn_;
 
         FlatButton *editFieldsBtn_;
@@ -113,6 +120,7 @@ private:
         TopSection *topSection_;
 
         QComboBox *accessCombo;
+        Toggle *encryptionToggle_;
 };
 
 } // dialogs
diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h
index 9997ec1d..f055f217 100644
--- a/include/timeline/TimelineItem.h
+++ b/include/timeline/TimelineItem.h
@@ -193,16 +193,17 @@ public:
         QString eventId() const { return event_id_; }
         void setEventId(const QString &event_id) { event_id_ = event_id; }
         void markReceived();
+        bool isReceived() { return isReceived_; };
         void setRoomId(QString room_id) { room_id_ = room_id; }
-        void sendReadReceipt() const
-        {
-                if (!event_id_.isEmpty())
-                        http::client()->readEvent(room_id_, event_id_);
-        }
+        void sendReadReceipt() const;
 
         //! 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;
@@ -225,6 +226,10 @@ private:
         void setupAvatarLayout(const QString &userName);
         void setupSimpleLayout();
 
+        //! Whether or not the event associated with the widget
+        //! has been acknowledged by the server.
+        bool isReceived_ = false;
+
         QString replaceEmoji(const QString &body);
         QString event_id_;
         QString room_id_;
diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h
index e6e35ccb..a86c0286 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>
@@ -34,6 +33,19 @@
 #include "ScrollBar.h"
 #include "TimelineItem.h"
 
+class StateKeeper
+{
+public:
+        StateKeeper(std::function<void()> &&fn)
+          : fn_(std::move(fn))
+        {}
+
+        ~StateKeeper() { fn_(); }
+
+private:
+        std::function<void()> fn_;
+};
+
 class FloatingButton;
 struct DescInfo;
 
@@ -42,33 +54,44 @@ 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)
-        {}
+        bool is_encrypted = false;
 };
 
+template<class MessageT>
+MessageT
+toRoomMessage(const PendingMessage &) = delete;
+
+template<>
+mtx::events::msg::Audio
+toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m);
+
+template<>
+mtx::events::msg::Emote
+toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m);
+
+template<>
+mtx::events::msg::File
+toRoomMessage<mtx::events::msg::File>(const PendingMessage &);
+
+template<>
+mtx::events::msg::Image
+toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m);
+
+template<>
+mtx::events::msg::Text
+toRoomMessage<mtx::events::msg::Text>(const PendingMessage &);
+
+template<>
+mtx::events::msg::Video
+toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m);
+
 // In which place new TimelineItems should be inserted.
 enum class TimelineDirection
 {
@@ -129,7 +152,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 +165,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 +191,25 @@ private:
 
         QWidget *relativeWidget(TimelineItem *item, int dt) const;
 
+        TimelineEvent parseEncryptedEvent(
+          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
+
+        void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+                               const std::string &room_key,
+                               const DevicePublicKeys &pks,
+                               const std::string &user_id,
+                               const std::string &device_id,
+                               const mtx::responses::ClaimKeys &res,
+                               mtx::http::RequestErr err);
+
+        //! Callback for all message sending.
+        void sendRoomMessageHandler(const std::string &txn_id,
+                                    const mtx::responses::EventId &res,
+                                    mtx::http::RequestErr err);
+        void prepareEncryptedMessage(const PendingMessage &msg);
+
+        //! 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 +275,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); }
 
@@ -315,14 +362,18 @@ TimelineView::addUserMessage(const QString &url,
 
         lastMessageDirection_ = TimelineDirection::Bottom;
 
-        QApplication::processEvents();
-
         // 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     = http::v2::client()->generate_txn_id();
+        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 +402,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 +427,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 308b83aa..9e31ecbf 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 b31385d1..7b0781a2 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 09181d32..66543e79 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 b17b2d8b..e9d823f4 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 49e52a82..391f57d9 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) {
+                          nhlog::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 c055ab05..397dd05f 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,29 +26,46 @@
 #include <variant.hpp>
 
 #include "Cache.h"
+#include "Logging.hpp"
 #include "Utils.h"
 
 //! Should be changed when a breaking change occurs in the cache format.
 //! This will reset client's data.
-static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.05.11");
+static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.06.10");
+static const std::string SECRET("secret");
 
 static const lmdb::val NEXT_BATCH_KEY("next_batch");
+static const lmdb::val OLM_ACCOUNT_KEY("olm_account");
 static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version");
 
 //! Cache databases and their format.
 //!
 //! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc).
 //! Format: room_id -> RoomInfo
-static constexpr const char *ROOMS_DB   = "rooms";
-static constexpr const char *INVITES_DB = "invites";
+constexpr auto ROOMS_DB("rooms");
+constexpr auto INVITES_DB("invites");
 //! Keeps already downloaded media for reuse.
 //! Format: matrix_url -> binary data.
-static constexpr const char *MEDIA_DB = "media";
+constexpr auto MEDIA_DB("media");
 //! Information that  must be kept between sync requests.
-static constexpr const char *SYNC_STATE_DB = "sync_state";
+constexpr auto SYNC_STATE_DB("sync_state");
 //! Read receipts per room/event.
-static constexpr const char *READ_RECEIPTS_DB = "read_receipts";
-static constexpr const char *NOTIFICATIONS_DB = "sent_notifications";
+constexpr auto READ_RECEIPTS_DB("read_receipts");
+constexpr auto NOTIFICATIONS_DB("sent_notifications");
+
+//! Encryption related databases.
+
+//! user_id -> list of devices
+constexpr auto DEVICES_DB("devices");
+//! device_id -> device keys
+constexpr auto DEVICE_KEYS_DB("device_keys");
+//! room_ids that have encryption enabled.
+constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms");
+
+//! room_id -> pickled OlmInboundGroupSession
+constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions");
+//! MegolmSessionIndex -> pickled OlmOutboundGroupSession
+constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions");
 
 using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
 using Receipts       = std::map<std::string, std::map<std::string, uint64_t>>;
@@ -62,8 +78,15 @@ namespace cache {
 void
 init(const QString &user_id)
 {
-        if (!instance_)
-                instance_ = std::make_unique<Cache>(user_id);
+        qRegisterMetaType<SearchResult>();
+        qRegisterMetaType<QVector<SearchResult>>();
+        qRegisterMetaType<RoomMember>();
+        qRegisterMetaType<RoomSearchResult>();
+        qRegisterMetaType<RoomInfo>();
+        qRegisterMetaType<QMap<QString, RoomInfo>>();
+        qRegisterMetaType<std::map<QString, RoomInfo>>();
+
+        instance_ = std::make_unique<Cache>(user_id);
 }
 
 Cache *
@@ -71,7 +94,7 @@ client()
 {
         return instance_.get();
 }
-}
+} // namespace cache
 
 Cache::Cache(const QString &userId, QObject *parent)
   : QObject{parent}
@@ -82,15 +105,21 @@ Cache::Cache(const QString &userId, QObject *parent)
   , mediaDb_{0}
   , readReceiptsDb_{0}
   , notificationsDb_{0}
+  , devicesDb_{0}
+  , deviceKeysDb_{0}
+  , inboundMegolmSessionDb_{0}
+  , outboundMegolmSessionDb_{0}
   , localUserId_{userId}
-{}
+{
+        setup();
+}
 
 void
 Cache::setup()
 {
-        qDebug() << "Setting up cache";
+        nhlog::db()->debug("setting up cache");
 
-        auto statePath = QString("%1/%2/state")
+        auto statePath = QString("%1/%2")
                            .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
                            .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
 
@@ -105,7 +134,7 @@ Cache::setup()
         env_.set_max_dbs(1024UL);
 
         if (isInitial) {
-                qDebug() << "First time initializing LMDB";
+                nhlog::db()->info("initializing LMDB");
 
                 if (!QDir().mkpath(statePath)) {
                         throw std::runtime_error(
@@ -121,7 +150,7 @@ Cache::setup()
                                                  std::string(e.what()));
                 }
 
-                qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what();
+                nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
 
                 QDir stateDir(statePath);
 
@@ -141,30 +170,315 @@ Cache::setup()
         mediaDb_         = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
         readReceiptsDb_  = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
         notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
+
+        // Device management
+        devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
+        deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
+
+        // Session management
+        inboundMegolmSessionDb_  = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+        outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+
         txn.commit();
+}
 
-        qRegisterMetaType<RoomInfo>();
+void
+Cache::setEncryptedRoom(const std::string &room_id)
+{
+        nhlog::db()->info("mark room {} as encrypted", room_id);
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+        lmdb::dbi_put(txn, db, lmdb::val(room_id), lmdb::val("0"));
+        txn.commit();
+}
+
+bool
+Cache::isRoomEncrypted(const std::string &room_id)
+{
+        lmdb::val unused;
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+        auto res = lmdb::dbi_get(txn, db, lmdb::val(room_id), unused);
+        txn.commit();
+
+        return res;
 }
 
+//
+// Device Management
+//
+
+//
+// Session Management
+//
+
 void
-Cache::saveImage(const QString &url, const QByteArray &image)
+Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
+                                mtx::crypto::InboundGroupSessionPtr session)
 {
-        auto key = url.toUtf8();
+        using namespace mtx::crypto;
+        const auto key     = index.to_hash();
+        const auto pickled = pickle<InboundSessionObject>(session.get(), SECRET);
+
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled));
+        txn.commit();
+
+        {
+                std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+                session_storage.group_inbound_sessions[key] = std::move(session);
+        }
+}
+
+OlmInboundGroupSession *
+Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
+{
+        std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+        return session_storage.group_inbound_sessions[index.to_hash()].get();
+}
+
+bool
+Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept
+{
+        std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+        return session_storage.group_inbound_sessions.find(index.to_hash()) !=
+               session_storage.group_inbound_sessions.end();
+}
+
+void
+Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index)
+{
+        using namespace mtx::crypto;
+
+        if (!outboundMegolmSessionExists(room_id))
+                return;
+
+        OutboundGroupSessionData data;
+        OlmOutboundGroupSession *session;
+        {
+                std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+                data    = session_storage.group_outbound_session_data[room_id];
+                session = session_storage.group_outbound_sessions[room_id].get();
+
+                // Update with the current message.
+                data.message_index                                   = message_index;
+                session_storage.group_outbound_session_data[room_id] = data;
+        }
+
+        // Save the updated pickled data for the session.
+        json j;
+        j["data"]    = data;
+        j["session"] = pickle<OutboundSessionObject>(session, SECRET);
+
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump()));
+        txn.commit();
+}
+
+void
+Cache::saveOutboundMegolmSession(const std::string &room_id,
+                                 const OutboundGroupSessionData &data,
+                                 mtx::crypto::OutboundGroupSessionPtr session)
+{
+        using namespace mtx::crypto;
+        const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET);
+
+        json j;
+        j["data"]    = data;
+        j["session"] = pickled;
+
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump()));
+        txn.commit();
+
+        {
+                std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+                session_storage.group_outbound_session_data[room_id] = data;
+                session_storage.group_outbound_sessions[room_id]     = std::move(session);
+        }
+}
+
+bool
+Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
+{
+        std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+        return (session_storage.group_outbound_sessions.find(room_id) !=
+                session_storage.group_outbound_sessions.end()) &&
+               (session_storage.group_outbound_session_data.find(room_id) !=
+                session_storage.group_outbound_session_data.end());
+}
+
+OutboundGroupSessionDataRef
+Cache::getOutboundMegolmSession(const std::string &room_id)
+{
+        std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+        return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(),
+                                           session_storage.group_outbound_session_data[room_id]};
+}
+
+//
+// OLM sessions.
+//
+
+void
+Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session)
+{
+        using namespace mtx::crypto;
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getOlmSessionsDb(txn, curve25519);
+
+        const auto pickled    = pickle<SessionObject>(session.get(), SECRET);
+        const auto session_id = mtx::crypto::session_id(session.get());
+
+        lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(pickled));
+
+        txn.commit();
+}
+
+boost::optional<mtx::crypto::OlmSessionPtr>
+Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
+{
+        using namespace mtx::crypto;
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getOlmSessionsDb(txn, curve25519);
+
+        lmdb::val pickled;
+        bool found = lmdb::dbi_get(txn, db, lmdb::val(session_id), pickled);
+
+        txn.commit();
+
+        if (found) {
+                auto data = std::string(pickled.data(), pickled.size());
+                return unpickle<SessionObject>(data, SECRET);
+        }
+
+        return boost::none;
+}
+
+std::vector<std::string>
+Cache::getOlmSessions(const std::string &curve25519)
+{
+        using namespace mtx::crypto;
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getOlmSessionsDb(txn, curve25519);
+
+        std::string session_id, unused;
+        std::vector<std::string> res;
+
+        auto cursor = lmdb::cursor::open(txn, db);
+        while (cursor.get(session_id, unused, MDB_NEXT))
+                res.emplace_back(session_id);
+        cursor.close();
+
+        txn.commit();
+
+        return res;
+}
+
+void
+Cache::saveOlmAccount(const std::string &data)
+{
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, syncStateDb_, OLM_ACCOUNT_KEY, lmdb::val(data));
+        txn.commit();
+}
+
+void
+Cache::restoreSessions()
+{
+        using namespace mtx::crypto;
+
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        std::string key, value;
+
+        //
+        // Inbound Megolm Sessions
+        //
+        {
+                auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
+                while (cursor.get(key, value, MDB_NEXT)) {
+                        auto session = unpickle<InboundSessionObject>(value, SECRET);
+                        session_storage.group_inbound_sessions[key] = std::move(session);
+                }
+                cursor.close();
+        }
+
+        //
+        // Outbound Megolm Sessions
+        //
+        {
+                auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_);
+                while (cursor.get(key, value, MDB_NEXT)) {
+                        json obj;
+
+                        try {
+                                obj = json::parse(value);
+
+                                session_storage.group_outbound_session_data[key] =
+                                  obj.at("data").get<OutboundGroupSessionData>();
+
+                                auto session =
+                                  unpickle<OutboundSessionObject>(obj.at("session"), SECRET);
+                                session_storage.group_outbound_sessions[key] = std::move(session);
+                        } catch (const nlohmann::json::exception &e) {
+                                nhlog::db()->critical(
+                                  "failed to parse outbound megolm session data: {}", e.what());
+                        }
+                }
+                cursor.close();
+        }
+
+        txn.commit();
+
+        nhlog::db()->info("sessions restored");
+}
+
+std::string
+Cache::restoreOlmAccount()
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        lmdb::val pickled;
+        lmdb::dbi_get(txn, syncStateDb_, OLM_ACCOUNT_KEY, pickled);
+        txn.commit();
+
+        return std::string(pickled.data(), pickled.size());
+}
+
+//
+// Media Management
+//
+
+void
+Cache::saveImage(const std::string &url, const std::string &img_data)
+{
+        if (url.empty() || img_data.empty())
+                return;
 
         try {
                 auto txn = lmdb::txn::begin(env_);
 
                 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();
+                nhlog::db()->critical("saveImage: {}", e.what());
         }
 }
 
+void
+Cache::saveImage(const QString &url, const QByteArray &image)
+{
+        saveImage(url.toStdString(), std::string(image.constData(), image.length()));
+}
+
 QByteArray
 Cache::image(lmdb::txn &txn, const std::string &url) const
 {
@@ -180,7 +494,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);
+                nhlog::db()->critical("image: {}, {}", e.what(), url);
         }
 
         return QByteArray();
@@ -208,7 +522,7 @@ Cache::image(const QString &url) const
 
                 return QByteArray(image.data(), image.size());
         } catch (const lmdb::error &e) {
-                qCritical() << "image:" << e.what() << url;
+                nhlog::db()->critical("image: {} {}", e.what(), url.toStdString());
         }
 
         return QByteArray();
@@ -271,7 +585,7 @@ Cache::isInitialized() const
         return res;
 }
 
-QString
+std::string
 Cache::nextBatchToken() const
 {
         auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
@@ -281,16 +595,17 @@ 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";
-
-        if (!cacheDirectory_.isEmpty())
+        // TODO: We need to remove the env_ while not accepting new requests.
+        if (!cacheDirectory_.isEmpty()) {
                 QDir(cacheDirectory_).removeRecursively();
+                nhlog::db()->info("deleted cache files from disk");
+        }
 }
 
 bool
@@ -304,13 +619,14 @@ Cache::isFormatValid()
         txn.commit();
 
         if (!res)
-                return false;
+                return true;
 
         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.";
+                nhlog::db()->warn("breaking changes in the cache format. stored: {}, current: {}",
+                                  stored_version,
+                                  CURRENT_CACHE_FORMAT_VERSION);
                 return false;
         }
 
@@ -360,7 +676,7 @@ Cache::readReceipts(const QString &event_id, const QString &room_id)
                 }
 
         } catch (const lmdb::error &e) {
-                qCritical() << "readReceipts:" << e.what();
+                nhlog::db()->critical("readReceipts: {}", e.what());
         }
 
         return receipts;
@@ -410,7 +726,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();
+                        nhlog::db()->critical("updateReadReceipts: {}", e.what());
                 }
         }
 }
@@ -568,9 +884,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()));
+                        nhlog::db()->warn("failed to parse room info: room_id ({}), {}",
+                                          room_id,
+                                          std::string(data.data(), data.size()));
                 }
         }
 
@@ -584,7 +900,8 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
 {
         std::map<QString, RoomInfo> room_info;
 
-        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        // TODO This should be read only.
+        auto txn = lmdb::txn::begin(env_);
 
         for (const auto &room : rooms) {
                 lmdb::val data;
@@ -600,9 +917,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()));
+                                nhlog::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 +932,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()));
+                                        nhlog::db()->warn(
+                                          "failed to parse room info for invite: room_id ({}), {}",
+                                          room,
+                                          std::string(data.data(), data.size()));
                                 }
                         }
                 }
@@ -703,7 +1020,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
 
                         return QString::fromStdString(msg.content.url);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
                 }
         }
 
@@ -726,7 +1043,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
                         cursor.close();
                         return QString::fromStdString(m.avatar_url);
                 } catch (const json::exception &e) {
-                        qWarning() << QString::fromStdString(e.what());
+                        nhlog::db()->warn("failed to parse member info: {}", e.what());
                 }
         }
 
@@ -753,7 +1070,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());
+                        nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
                 }
         }
 
@@ -768,7 +1085,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());
+                        nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
+                                          e.what());
                 }
         }
 
@@ -784,7 +1102,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());
+                        nhlog::db()->warn("failed to parse member info: {}", e.what());
                 }
 
                 ii++;
@@ -828,7 +1146,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();
+                        nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
                 }
         }
         return JoinRule::Knock;
@@ -850,7 +1168,8 @@ 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();
+                        nhlog::db()->warn("failed to parse m.room.guest_access event: {}",
+                                          e.what());
                 }
         }
         return false;
@@ -874,7 +1193,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());
+                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
                 }
         }
 
@@ -897,7 +1216,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());
+                        nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
                 }
         }
 
@@ -914,7 +1233,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());
+                        nhlog::db()->warn("failed to parse member info: {}", e.what());
                 }
         }
 
@@ -939,7 +1258,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());
+                        nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
                 }
         }
 
@@ -956,7 +1275,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());
+                        nhlog::db()->warn("failed to parse member info: {}", e.what());
                 }
         }
 
@@ -981,7 +1300,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());
+                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
                 }
         }
 
@@ -1017,8 +1336,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()));
+                nhlog::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 +1374,7 @@ void
 Cache::populateMembers()
 {
         auto rooms = joinedRooms();
-        qDebug() << "loading" << rooms.size() << "rooms";
+        nhlog::db()->info("loading {} rooms", rooms.size());
 
         auto txn = lmdb::txn::begin(env_);
 
@@ -1182,7 +1502,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();
+                        nhlog::db()->warn("{}", e.what());
                 }
 
                 currentIndex += 1;
@@ -1253,7 +1573,8 @@ 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();
+                        nhlog::db()->warn("failed to parse m.room.power_levels event: {}",
+                                          e.what());
                 }
         }
 
@@ -1262,6 +1583,26 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
         return user_level >= min_event_level;
 }
 
+std::vector<std::string>
+Cache::roomMembers(const std::string &room_id)
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+        std::vector<std::string> members;
+        std::string user_id, unused;
+
+        auto db = getMembersDb(txn, room_id);
+
+        auto cursor = lmdb::cursor::open(txn, db);
+        while (cursor.get(user_id, unused, MDB_NEXT))
+                members.emplace_back(std::move(user_id));
+        cursor.close();
+
+        txn.commit();
+
+        return members;
+}
+
 QHash<QString, QString> Cache::DisplayNames;
 QHash<QString, QString> Cache::AvatarUrls;
 
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 9ae860fb..e543cdf9 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -16,15 +16,16 @@
  */
 
 #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 "Olm.hpp"
 #include "OverlayModal.h"
 #include "QuickSwitcher.h"
 #include "RoomList.h"
@@ -43,13 +44,16 @@
 #include "dialogs/ReadReceipts.h"
 #include "timeline/TimelineViewManager.h"
 
-constexpr int SYNC_RETRY_TIMEOUT         = 40 * 1000;
-constexpr int INITIAL_SYNC_RETRY_TIMEOUT = 240 * 1000;
+// TODO: Needs to be updated with an actual secret.
+static const std::string STORAGE_SECRET_KEY("secret");
 
-ChatPage *ChatPage::instance_ = nullptr;
+ChatPage *ChatPage::instance_             = nullptr;
+constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
+constexpr size_t MAX_ONETIME_KEYS         = 50;
 
 ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
   : QWidget(parent)
+  , isConnected_(true)
   , userSettings_{userSettings}
 {
         setObjectName("chatPage");
@@ -78,13 +82,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 +110,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 +128,82 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         typingRefresher_ = new QTimer(this);
         typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT);
 
+        connect(this, &ChatPage::connectionLost, this, [this]() {
+                nhlog::net()->info("connectivity lost");
+                isConnected_ = false;
+                http::v2::client()->shutdown();
+                text_input_->disableInput();
+        });
+        connect(this, &ChatPage::connectionRestored, this, [this]() {
+                nhlog::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();
+                                  nhlog::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 +229,34 @@ 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())
+                if (current_room_.isEmpty())
                         return;
 
-                http::client()->sendTypingNotification(current_room_);
+                http::v2::client()->stop_typing(
+                  current_room_.toStdString(), [](mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::net()->warn("failed to stop typing notifications: {}",
+                                                     err->matrix_error.error);
+                          }
+                  });
         });
 
         connect(view_manager_,
@@ -207,142 +279,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."));
+                                          nhlog::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."));
+                                          nhlog::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."));
+                                          nhlog::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."));
+                                          nhlog::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 +533,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 +552,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 +570,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();
+                        nhlog::db()->error("failed to retrieve invites: {}", e.what());
                 }
 
                 view_manager_->initialize(rooms);
@@ -437,7 +589,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) {
+                                          nhlog::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 +611,24 @@ 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);
+        connect(this, &ChatPage::tryDelayedSyncCb, this, [this]() {
+                QTimer::singleShot(5000, this, &ChatPage::trySync);
+        });
+
+        connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
 
-        qRegisterMetaType<std::map<QString, RoomInfo>>();
-        qRegisterMetaType<QMap<QString, RoomInfo>>();
-        qRegisterMetaType<mtx::responses::Rooms>();
-        qRegisterMetaType<std::vector<std::string>>();
+        instance_ = this;
 }
 
 void
@@ -462,6 +639,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,90 +680,71 @@ 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) {
+                nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
+                                      userid.toStdString());
+        }
 
-        cache::init(userid);
+        http::v2::client()->set_server(homeserver.toStdString());
+        http::v2::client()->set_access_token(token.toStdString());
+
+        // The Olm client needs the user_id & device_id that will be included
+        // in the generated payloads & keys.
+        olm::client()->set_user_id(http::v2::client()->user_id().to_string());
+        olm::client()->set_device_id(http::v2::client()->device_id());
 
         try {
-                cache::client()->setup();
+                cache::init(userid);
+
+                const bool isInitialized = cache::client()->isInitialized();
+                const bool isValid       = cache::client()->isFormatValid();
 
-                if (!cache::client()->isFormatValid()) {
+                if (isInitialized && !isValid) {
+                        nhlog::db()->warn("breaking changes in cache");
+                        // TODO: Deleting session data but keep using the
+                        //	 same device doesn't work.
                         cache::client()->deleteData();
-                        cache::client()->setup();
-                        cache::client()->setCurrentFormat();
-                }
 
-                if (cache::client()->isInitialized()) {
+                        cache::init(userid);
+                        cache::client()->setCurrentFormat();
+                } else if (isInitialized) {
                         loadStateFromCache();
                         return;
                 }
         } catch (const lmdb::error &e) {
-                qCritical() << "Cache failure" << e.what();
+                nhlog::db()->critical("failure during boot: {}", e.what());
                 cache::client()->deleteData();
-                qInfo() << "Falling back to initial sync ...";
+                nhlog::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;
-                }
+        try {
+                // It's the first time syncing with this device
+                // There isn't a saved olm account to restore.
+                nhlog::crypto()->info("creating new olm account");
+                olm::client()->create_new_account();
+                cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY));
+        } catch (const lmdb::error &e) {
+                nhlog::crypto()->critical("failed to save olm account {}", e.what());
+                emit dropToLoginPageCb(QString::fromStdString(e.what()));
+                return;
+        } catch (const mtx::crypto::olm_exception &e) {
+                nhlog::crypto()->critical("failed to create new olm account {}", e.what());
+                emit dropToLoginPageCb(QString::fromStdString(e.what()));
+                return;
+        }
 
-                emit continueSync(cache::client()->nextBatchToken());
-                emit contentLoaded();
-        });
+        getProfileInfo();
+        tryInitialSync();
 }
 
 void
@@ -586,41 +757,6 @@ ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img)
 }
 
 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)
 {
         for (int i = 0; i < own_communities.size(); i++) {
@@ -636,7 +772,7 @@ void
 ChatPage::changeTopRoomInfo(const QString &room_id)
 {
         if (room_id.isEmpty()) {
-                qWarning() << "can't switch to empty room_id";
+                nhlog::ui()->warn("cannot switch to empty room_id");
                 return;
         }
 
@@ -660,7 +796,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();
+                nhlog::ui()->error("failed to change top bar room info: {}", e.what());
         }
 
         current_room_ = room_id;
@@ -681,22 +817,37 @@ ChatPage::showUnreadMessageNotification(int count)
 void
 ChatPage::loadStateFromCache()
 {
-        qDebug() << "restoring state from cache";
+        nhlog::db()->info("restoring state from cache");
+
+        getProfileInfo();
 
         QtConcurrent::run([this]() {
                 try {
+                        cache::client()->restoreSessions();
+                        olm::client()->load(cache::client()->restoreOlmAccount(),
+                                            STORAGE_SECRET_KEY);
+
                         cache::client()->populateMembers();
 
                         emit initializeEmptyViews(cache::client()->joinedRooms());
                         emit initializeRoomList(cache::client()->roomInfo());
+                } catch (const mtx::crypto::olm_exception &e) {
+                        nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
+                        emit dropToLoginPageCb(
+                          tr("Failed to restore OLM account. Please login again."));
+                        return;
                 } catch (const lmdb::error &e) {
-                        std::cout << "load cache error:" << e.what() << '\n';
-                        // TODO Clear cache and restart.
+                        nhlog::db()->critical("failed to restore cache: {}", e.what());
+                        emit dropToLoginPageCb(
+                          tr("Failed to restore save data. Please login again."));
                         return;
                 }
 
+                nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+                nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+
                 // Start receiving events.
-                emit continueSync(cache::client()->nextBatchToken());
+                emit trySyncCb();
 
                 // Check periodically if the timelines have been loaded.
                 emit startConsesusTimer();
@@ -740,7 +891,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();
+                nhlog::db()->critical("failure while removing room: {}", e.what());
                 // TODO: Notify the user.
         }
 
@@ -824,33 +975,6 @@ ChatPage::setGroupViewState(bool isEnabled)
 }
 
 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)
 {
         room_list_->updateUnreadMessageCount(room_id, notification_count);
@@ -886,7 +1010,321 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res)
                                   utils::event_body(item.event));
                         }
                 } catch (const lmdb::error &e) {
-                        qWarning() << e.what();
+                        nhlog::db()->warn("error while sending desktop notification: {}", e.what());
                 }
         }
 }
+
+void
+ChatPage::tryInitialSync()
+{
+        nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+        nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+
+        // Upload one time keys for the device.
+        nhlog::crypto()->info("generating one time keys");
+        olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS);
+
+        http::v2::client()->upload_keys(
+          olm::client()->create_upload_keys_request(),
+          [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          const int status_code = static_cast<int>(err->status_code);
+                          nhlog::crypto()->critical("failed to upload one time keys: {} {}",
+                                                    err->matrix_error.error,
+                                                    status_code);
+                          // TODO We should have a timeout instead of keeping hammering the server.
+                          emit tryInitialSyncCb();
+                          return;
+                  }
+
+                  olm::client()->mark_keys_as_published();
+                  for (const auto &entry : res.one_time_key_counts)
+                          nhlog::net()->info(
+                            "uploaded {} {} one-time keys", entry.second, entry.first);
+
+                  nhlog::net()->info("trying initial sync");
+
+                  mtx::http::SyncOpts opts;
+                  opts.timeout = 0;
+                  http::v2::client()->sync(opts,
+                                           std::bind(&ChatPage::initialSyncHandler,
+                                                     this,
+                                                     std::placeholders::_1,
+                                                     std::placeholders::_2));
+          });
+}
+
+void
+ChatPage::trySync()
+{
+        mtx::http::SyncOpts opts;
+
+        if (!connectivityTimer_.isActive())
+                connectivityTimer_.start();
+
+        try {
+                opts.since = cache::client()->nextBatchToken();
+        } catch (const lmdb::error &e) {
+                nhlog::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);
+
+                          nhlog::net()->error("sync error: {} {}", status_code, err_code);
+
+                          if (status_code <= 0 || status_code >= 600) {
+                                  if (!http::v2::is_logged_in())
+                                          return;
+
+                                  emit tryDelayedSyncCb();
+                                  return;
+                          }
+
+                          switch (status_code) {
+                          case 502:
+                          case 504:
+                          case 524: {
+                                  emit trySyncCb();
+                                  return;
+                          }
+                          default: {
+                                  if (!http::v2::is_logged_in())
+                                          return;
+
+                                  if (err->matrix_error.errcode ==
+                                      mtx::errors::ErrorCode::M_UNKNOWN_TOKEN)
+                                          emit dropToLoginPageCb(msg);
+                                  else
+                                          emit tryDelayedSyncCb();
+
+                                  return;
+                          }
+                          }
+                  }
+
+                  nhlog::net()->debug("sync completed: {}", res.next_batch);
+
+                  // Ensure that we have enough one-time keys available.
+                  ensureOneTimeKeyCount(res.device_one_time_keys_count);
+
+                  // TODO: fine grained error handling
+                  try {
+                          cache::client()->saveState(res);
+                          olm::handle_to_device_messages(res.to_device);
+
+                          emit syncUI(res.rooms);
+
+                          auto updates = cache::client()->roomUpdates(res);
+
+                          emit syncTopBar(updates);
+                          emit syncRoomlist(updates);
+                  } catch (const lmdb::error &e) {
+                          nhlog::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) {
+                          nhlog::net()->warn("failed to send typing notification: {}",
+                                             err->matrix_error.error);
+                  }
+          });
+}
+
+void
+ChatPage::initialSyncHandler(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);
+
+                nhlog::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;
+                }
+                }
+        }
+
+        nhlog::net()->info("initial sync completed");
+
+        try {
+                cache::client()->saveState(res);
+
+                olm::handle_to_device_messages(res.to_device);
+
+                emit initializeViews(std::move(res.rooms));
+                emit initializeRoomList(cache::client()->roomInfo());
+        } catch (const lmdb::error &e) {
+                nhlog::db()->error("{}", e.what());
+                emit tryInitialSyncCb();
+                return;
+        }
+
+        emit trySyncCb();
+        emit contentLoaded();
+}
+
+void
+ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
+{
+        for (const auto &entry : counts) {
+                if (entry.second < MAX_ONETIME_KEYS) {
+                        const int nkeys = MAX_ONETIME_KEYS - entry.second;
+
+                        nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first);
+                        olm::client()->generate_one_time_keys(nkeys);
+
+                        http::v2::client()->upload_keys(
+                          olm::client()->create_upload_keys_request(),
+                          [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
+                                  if (err) {
+                                          nhlog::crypto()->warn(
+                                            "failed to update one-time keys: {} {}",
+                                            err->matrix_error.error,
+                                            static_cast<int>(err->status_code));
+                                          return;
+                                  }
+
+                                  olm::client()->mark_keys_as_published();
+                          });
+                }
+        }
+}
+
+void
+ChatPage::getProfileInfo()
+{
+        QSettings settings;
+        const auto userid = settings.value("auth/user_id").toString().toStdString();
+
+        http::v2::client()->get_profile(
+          userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::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) {
+                                    nhlog::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();
+}
diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc
index 0d7f5aab..39e9a7fe 100644
--- a/src/CommunitiesList.cc
+++ b/src/CommunitiesList.cc
@@ -1,4 +1,6 @@
 #include "CommunitiesList.h"
+#include "Cache.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,40 @@ 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;
+        }
+
+        if (avatarUrl.isEmpty())
+                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) {
+                          nhlog::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 00000000..bccbe389
--- /dev/null
+++ b/src/Logging.cpp
@@ -0,0 +1,59 @@
+#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> crypto_logger = nullptr;
+std::shared_ptr<spdlog::logger> ui_logger     = nullptr;
+
+constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
+constexpr auto MAX_LOG_FILES = 3;
+}
+
+namespace nhlog {
+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));
+        ui_logger  = std::make_shared<spdlog::logger>("ui", std::begin(sinks), std::end(sinks));
+        db_logger  = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
+        crypto_logger =
+          std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
+}
+
+std::shared_ptr<spdlog::logger>
+ui()
+{
+        return ui_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+net()
+{
+        return net_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+db()
+{
+        return db_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+crypto()
+{
+        return crypto_logger;
+}
+}
diff --git a/src/LoginPage.cc b/src/LoginPage.cc
index c7f9b042..d695a759 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 c46cbff1..088bb5c0 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"
@@ -54,9 +54,6 @@ MainWindow::MainWindow(QWidget *parent)
         setWindowTitle("nheko");
         setObjectName("MainWindow");
 
-        // Initialize the http client.
-        http::init();
-
         restoreWindowSize();
 
         QFont font("Open Sans");
@@ -124,21 +121,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);
 
@@ -156,8 +145,21 @@ MainWindow::MainWindow(QWidget *parent)
                 QString token       = settings.value("auth/access_token").toString();
                 QString home_server = settings.value("auth/home_server").toString();
                 QString user_id     = settings.value("auth/user_id").toString();
+                QString device_id   = settings.value("auth/device_id").toString();
+
+                http::v2::client()->set_access_token(token.toStdString());
+                http::v2::client()->set_server(home_server.toStdString());
+                http::v2::client()->set_device_id(device_id.toStdString());
+
+                try {
+                        using namespace mtx::identifiers;
+                        http::v2::client()->set_user(parse<User>(user_id.toStdString()));
+                } catch (const std::invalid_argument &e) {
+                        nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
+                                              user_id.toStdString());
+                }
 
-                showChatPage(user_id, home_server, token);
+                showChatPage();
         }
 }
 
@@ -216,12 +218,19 @@ 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 device_id  = QString::fromStdString(http::v2::client()->device_id());
+        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);
         settings.setValue("auth/user_id", userid);
+        settings.setValue("auth/device_id", device_id);
 
         showOverlayProgressBar();
 
@@ -317,7 +326,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 a9720d10..d4ab8e33 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -1,1368 +1,38 @@
-/*
- * 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 <memory>
+
 namespace {
-std::unique_ptr<MatrixClient> instance_ = nullptr;
+auto v2_client_ = std::make_shared<mtx::http::Client>();
 }
 
 namespace http {
+namespace v2 {
 
-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);
-        });
-}
-
-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);
-        });
-}
-
-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));
+        return v2_client_.get();
 }
 
-void
-MatrixClient::readEvent(const QString &room_id, const QString &event_id)
+bool
+is_logged_in()
 {
-        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;
+        return !v2_client_->access_token().empty();
 }
 
-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;
-}
+} // namespace v2
 
 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/Olm.cpp b/src/Olm.cpp
new file mode 100644
index 00000000..814fce18
--- /dev/null
+++ b/src/Olm.cpp
@@ -0,0 +1,228 @@
+#include "Olm.hpp"
+
+#include "Cache.h"
+#include "Logging.hpp"
+
+using namespace mtx::crypto;
+
+namespace {
+auto client_ = std::make_unique<mtx::crypto::OlmClient>();
+}
+
+namespace olm {
+
+mtx::crypto::OlmClient *
+client()
+{
+        return client_.get();
+}
+
+void
+handle_to_device_messages(const std::vector<nlohmann::json> &msgs)
+{
+        if (msgs.empty())
+                return;
+
+        nhlog::crypto()->info("received {} to_device messages", msgs.size());
+
+        for (const auto &msg : msgs) {
+                try {
+                        OlmMessage olm_msg = msg;
+                        handle_olm_message(std::move(olm_msg));
+                } catch (const nlohmann::json::exception &e) {
+                        nhlog::crypto()->warn(
+                          "parsing error for olm message: {} {}", e.what(), msg.dump(2));
+                } catch (const std::invalid_argument &e) {
+                        nhlog::crypto()->warn(
+                          "validation error for olm message: {} {}", e.what(), msg.dump(2));
+                }
+        }
+}
+
+void
+handle_olm_message(const OlmMessage &msg)
+{
+        nhlog::crypto()->info("sender    : {}", msg.sender);
+        nhlog::crypto()->info("sender_key: {}", msg.sender_key);
+
+        const auto my_key = olm::client()->identity_keys().curve25519;
+
+        for (const auto &cipher : msg.ciphertext) {
+                // We skip messages not meant for the current device.
+                if (cipher.first != my_key)
+                        continue;
+
+                const auto type = cipher.second.type;
+                nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
+
+                auto payload = try_olm_decryption(msg.sender_key, cipher.second);
+
+                if (payload) {
+                        nhlog::crypto()->info("decrypted olm payload: {}", payload.value().dump(2));
+                        create_inbound_megolm_session(msg.sender, msg.sender_key, payload.value());
+                        return;
+                }
+
+                // Not a PRE_KEY message
+                if (cipher.second.type != 0) {
+                        // TODO: log that it should have matched something
+                        return;
+                }
+
+                handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second);
+        }
+}
+
+void
+handle_pre_key_olm_message(const std::string &sender,
+                           const std::string &sender_key,
+                           const OlmCipherContent &content)
+{
+        nhlog::crypto()->info("opening olm session with {}", sender);
+
+        OlmSessionPtr inbound_session = nullptr;
+        try {
+                inbound_session = olm::client()->create_inbound_session(content.body);
+
+                // We also remove the one time key used to establish that
+                // session so we'll have to update our copy of the account object.
+                cache::client()->saveOlmAccount(olm::client()->save("secret"));
+        } catch (const olm_exception &e) {
+                nhlog::crypto()->critical(
+                  "failed to create inbound session with {}: {}", sender, e.what());
+                return;
+        }
+
+        if (!matches_inbound_session_from(inbound_session.get(), sender_key, content.body)) {
+                nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})",
+                                      sender);
+                return;
+        }
+
+        mtx::crypto::BinaryBuf output;
+        try {
+                output =
+                  olm::client()->decrypt_message(inbound_session.get(), content.type, content.body);
+        } catch (const olm_exception &e) {
+                nhlog::crypto()->critical(
+                  "failed to decrypt olm message {}: {}", content.body, e.what());
+                return;
+        }
+
+        auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
+        nhlog::crypto()->info("decrypted message: \n {}", plaintext.dump(2));
+
+        try {
+                cache::client()->saveOlmSession(sender_key, std::move(inbound_session));
+        } catch (const lmdb::error &e) {
+                nhlog::db()->warn(
+                  "failed to save inbound olm session from {}: {}", sender, e.what());
+        }
+
+        create_inbound_megolm_session(sender, sender_key, plaintext);
+}
+
+mtx::events::msg::Encrypted
+encrypt_group_message(const std::string &room_id,
+                      const std::string &device_id,
+                      const std::string &body)
+{
+        using namespace mtx::events;
+
+        // Always chech before for existence.
+        auto res     = cache::client()->getOutboundMegolmSession(room_id);
+        auto payload = olm::client()->encrypt_group_message(res.session, body);
+
+        // Prepare the m.room.encrypted event.
+        msg::Encrypted data;
+        data.ciphertext = std::string((char *)payload.data(), payload.size());
+        data.sender_key = olm::client()->identity_keys().curve25519;
+        data.session_id = res.data.session_id;
+        data.device_id  = device_id;
+
+        auto message_index = olm_outbound_group_session_message_index(res.session);
+        nhlog::crypto()->info("next message_index {}", message_index);
+
+        // We need to re-pickle the session after we send a message to save the new message_index.
+        cache::client()->updateOutboundMegolmSession(room_id, message_index);
+
+        return data;
+}
+
+boost::optional<json>
+try_olm_decryption(const std::string &sender_key, const OlmCipherContent &msg)
+{
+        auto session_ids = cache::client()->getOlmSessions(sender_key);
+
+        for (const auto &id : session_ids) {
+                auto session = cache::client()->getOlmSession(sender_key, id);
+
+                if (!session)
+                        continue;
+
+                mtx::crypto::BinaryBuf text;
+
+                try {
+                        text = olm::client()->decrypt_message(session->get(), msg.type, msg.body);
+                        cache::client()->saveOlmSession(id, std::move(session.value()));
+
+                } catch (const olm_exception &e) {
+                        nhlog::crypto()->info("failed to decrypt olm message ({}, {}) with {}: {}",
+                                              msg.type,
+                                              sender_key,
+                                              id,
+                                              e.what());
+                        continue;
+                } catch (const lmdb::error &e) {
+                        nhlog::crypto()->critical("failed to save session: {}", e.what());
+                        return {};
+                }
+
+                try {
+                        return json::parse(std::string((char *)text.data(), text.size()));
+                } catch (const json::exception &e) {
+                        nhlog::crypto()->critical("failed to parse the decrypted session msg: {}",
+                                                  e.what());
+                }
+        }
+
+        return {};
+}
+
+void
+create_inbound_megolm_session(const std::string &sender,
+                              const std::string &sender_key,
+                              const nlohmann::json &payload)
+{
+        std::string room_id, session_id, session_key;
+
+        try {
+                room_id     = payload.at("content").at("room_id");
+                session_id  = payload.at("content").at("session_id");
+                session_key = payload.at("content").at("session_key");
+        } catch (const nlohmann::json::exception &e) {
+                nhlog::crypto()->critical(
+                  "failed to parse plaintext olm message: {} {}", e.what(), payload.dump(2));
+                return;
+        }
+
+        MegolmSessionIndex index;
+        index.room_id    = room_id;
+        index.session_id = session_id;
+        index.sender_key = sender_key;
+
+        try {
+                auto megolm_session = olm::client()->init_inbound_group_session(session_key);
+                cache::client()->saveInboundMegolmSession(index, std::move(megolm_session));
+        } catch (const lmdb::error &e) {
+                nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
+                return;
+        } catch (const olm_exception &e) {
+                nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what());
+                return;
+        }
+
+        nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender);
+}
+
+} // namespace olm
diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc
index 7d80b727..db52e101 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) {
+                                                      nhlog::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) {
+                                                    nhlog::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;
+                          }
+
+                          nhlog::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 e7c5ef30..b5bcdad6 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) {
+                                  nhlog::net()->warn(
+                                    "failed to download room avatar: {} {} {}",
+                                    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";
+                nhlog::ui()->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";
+        nhlog::ui()->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";
+                nhlog::ui()->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;
+                nhlog::ui()->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;
+                nhlog::ui()->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 f3753971..acb33fa7 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/PreviewUploadOverlay.cc b/src/dialogs/PreviewUploadOverlay.cc
index 3c44e911..e01d2b17 100644
--- a/src/dialogs/PreviewUploadOverlay.cc
+++ b/src/dialogs/PreviewUploadOverlay.cc
@@ -17,7 +17,6 @@
 
 #include <QApplication>
 #include <QBuffer>
-#include <QDebug>
 #include <QFile>
 #include <QFileInfo>
 #include <QHBoxLayout>
@@ -25,14 +24,15 @@
 #include <QVBoxLayout>
 
 #include "Config.h"
+#include "Logging.hpp"
 #include "Utils.h"
 
 #include "dialogs/PreviewUploadOverlay.h"
 
 using namespace dialogs;
 
-static constexpr const char *DEFAULT = "Upload %1?";
-static constexpr const char *ERROR   = "Failed to load image type '%1'. Continue upload?";
+constexpr const char *DEFAULT = "Upload %1?";
+constexpr const char *ERR_MSG = "Failed to load image type '%1'. Continue upload?";
 
 PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
   : QWidget{parent}
@@ -105,7 +105,7 @@ PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64
 {
         if (mediaType_ == "image") {
                 if (!image_.loadFromData(data_)) {
-                        titleLabel_.setText(QString{tr(ERROR)}.arg(type));
+                        titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
                 } else {
                         titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_));
                 }
@@ -142,8 +142,9 @@ PreviewUploadOverlay::setPreview(const QString &path)
         QFile file{path};
 
         if (!file.open(QIODevice::ReadOnly)) {
-                qWarning() << "Failed to open file from:" << path;
-                qWarning() << "Reason:" << file.errorString();
+                nhlog::ui()->warn("Failed to open file ({}): {}",
+                                  path.toStdString(),
+                                  file.errorString().toStdString());
                 close();
                 return;
         }
@@ -152,7 +153,7 @@ PreviewUploadOverlay::setPreview(const QString &path)
         auto mime = db.mimeTypeForFileNameAndData(path, &file);
 
         if ((data_ = file.readAll()).isEmpty()) {
-                qWarning() << "Failed to read media:" << file.errorString();
+                nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString());
                 close();
                 return;
         }
diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp
index ba487cea..6b1143b5 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 4d2f304b..74d08478 100644
--- a/src/dialogs/RoomSettings.cpp
+++ b/src/dialogs/RoomSettings.cpp
@@ -1,16 +1,20 @@
 #include "Avatar.h"
+#include "ChatPage.h"
 #include "Config.h"
 #include "FlatButton.h"
+#include "Logging.hpp"
 #include "MatrixClient.h"
 #include "Painter.h"
 #include "TextField.h"
 #include "Theme.h"
 #include "Utils.h"
 #include "dialogs/RoomSettings.hpp"
+#include "ui/ToggleButton.h"
 
 #include <QApplication>
 #include <QComboBox>
 #include <QLabel>
+#include <QMessageBox>
 #include <QPainter>
 #include <QPixmap>
 #include <QSettings>
@@ -67,6 +71,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 +103,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);
@@ -190,8 +192,8 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
         layout->setSpacing(15);
         layout->setMargin(20);
 
-        saveBtn_ = new FlatButton("SAVE", this);
-        saveBtn_->setFontSize(conf::btn::fontSize);
+        okBtn_ = new FlatButton(tr("OK"), this);
+        okBtn_->setFontSize(conf::btn::fontSize);
         cancelBtn_ = new FlatButton(tr("CANCEL"), this);
         cancelBtn_->setFontSize(conf::btn::fontSize);
 
@@ -199,7 +201,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
         btnLayout->setSpacing(0);
         btnLayout->setMargin(0);
         btnLayout->addStretch(1);
-        btnLayout->addWidget(saveBtn_);
+        btnLayout->addWidget(okBtn_);
         btnLayout->addWidget(cancelBtn_);
 
         auto notifOptionLayout_ = new QHBoxLayout;
@@ -238,6 +240,61 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
         accessOptionLayout->addWidget(accessLabel);
         accessOptionLayout->addWidget(accessCombo);
 
+        auto encryptionOptionLayout = new QHBoxLayout;
+        encryptionOptionLayout->setMargin(SettingsMargin);
+        auto encryptionLabel = new QLabel(tr("Encryption"), this);
+        encryptionLabel->setStyleSheet("font-size: 15px;");
+        encryptionToggle_ = new Toggle(this);
+        connect(encryptionToggle_, &Toggle::toggled, this, [this](bool isOn) {
+                if (isOn)
+                        return;
+
+                QFont font;
+                font.setPixelSize(conf::fontSize);
+
+                QMessageBox msgBox;
+                msgBox.setIcon(QMessageBox::Question);
+                msgBox.setFont(font);
+                msgBox.setWindowTitle(tr("End-to-End Encryption"));
+                msgBox.setText(tr(
+                  "Encryption is currently experimental and things might break unexpectedly. <br>"
+                  "Please take note that it can't be disabled afterwards."));
+                msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
+                msgBox.setDefaultButton(QMessageBox::Save);
+                int ret = msgBox.exec();
+
+                switch (ret) {
+                case QMessageBox::Ok: {
+                        encryptionToggle_->setState(false);
+                        encryptionToggle_->setEnabled(false);
+                        enableEncryption();
+                        break;
+                }
+                default: {
+                        encryptionToggle_->setState(true);
+                        encryptionToggle_->setEnabled(true);
+                        break;
+                }
+                }
+        });
+
+        encryptionOptionLayout->addWidget(encryptionLabel);
+        encryptionOptionLayout->addWidget(encryptionToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
+
+        // Disable encryption button.
+        if (usesEncryption_) {
+                encryptionToggle_->setState(false);
+                encryptionToggle_->setEnabled(false);
+        } else {
+                encryptionToggle_->setState(true);
+        }
+
+        // Hide encryption option for public rooms.
+        if (!usesEncryption_ && (info_.join_rule == JoinRule::Public)) {
+                encryptionToggle_->hide();
+                encryptionLabel->hide();
+        }
+
         QFont font;
         font.setPixelSize(18);
         font.setWeight(70);
@@ -257,10 +314,18 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
         layout->addLayout(editLayout_);
         layout->addLayout(notifOptionLayout_);
         layout->addLayout(accessOptionLayout);
+        layout->addLayout(encryptionOptionLayout);
         layout->addLayout(btnLayout);
 
         connect(cancelBtn_, &QPushButton::clicked, this, &RoomSettings::closing);
-        connect(saveBtn_, &QPushButton::clicked, this, &RoomSettings::saveSettings);
+        connect(okBtn_, &QPushButton::clicked, this, &RoomSettings::saveSettings);
+
+        connect(this, &RoomSettings::enableEncryptionError, this, [this](const QString &msg) {
+                encryptionToggle_->setState(true);
+                encryptionToggle_->setEnabled(true);
+
+                emit ChatPage::instance()->showNotification(msg);
+        });
 }
 
 void
@@ -273,7 +338,7 @@ RoomSettings::setupEditButton()
                 hasEditRights_ = cache::client()->hasEnoughPowerLevel(
                   {EventType::RoomName, EventType::RoomTopic}, room_id_.toStdString(), userId);
         } catch (const lmdb::error &e) {
-                qWarning() << "lmdb error" << e.what();
+                nhlog::db()->warn("lmdb error: {}", e.what());
         }
 
         constexpr int buttonSize = 36;
@@ -310,10 +375,12 @@ void
 RoomSettings::retrieveRoomInfo()
 {
         try {
-                info_ = cache::client()->singleRoomInfo(room_id_.toStdString());
+                usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString());
+                info_           = cache::client()->singleRoomInfo(room_id_.toStdString());
                 setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url)));
         } catch (const lmdb::error &e) {
-                qWarning() << "failed to retrieve room info from cache" << room_id_;
+                nhlog::db()->warn("failed to retrieve room info from cache: {}",
+                                  room_id_.toStdString());
         }
 }
 
@@ -342,6 +409,28 @@ RoomSettings::saveSettings()
 }
 
 void
+RoomSettings::enableEncryption()
+{
+        const auto room_id = room_id_.toStdString();
+        http::v2::client()->enable_encryption(
+          room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+                  if (err) {
+                          int status_code = static_cast<int>(err->status_code);
+                          nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
+                                             room_id,
+                                             err->matrix_error.error,
+                                             status_code);
+                          emit enableEncryptionError(
+                            tr("Failed to enable encryption: %1")
+                              .arg(QString::fromStdString(err->matrix_error.error)));
+                          return;
+                  }
+
+                  nhlog::net()->info("enabled encryption on room ({})", room_id);
+          });
+}
+
+void
 RoomSettings::paintEvent(QPaintEvent *)
 {
         QStyleOption opt;
diff --git a/src/main.cc b/src/main.cc
index bd3a212c..327ec587 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -17,20 +17,23 @@
 
 #include <QApplication>
 #include <QDesktopWidget>
+#include <QDir>
 #include <QFile>
 #include <QFontDatabase>
 #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"
@@ -47,28 +50,13 @@ screenCenter(int width, int height)
 }
 
 void
-setupProxy()
+createCacheDirectory()
 {
-        QSettings settings;
+        auto dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
 
-        /**
-          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);
+        if (!QDir().mkpath(dir)) {
+                throw std::runtime_error(
+                  ("Unable to create state directory:" + dir).toStdString().c_str());
         }
 }
 
@@ -133,7 +121,19 @@ 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();
+
+        createCacheDirectory();
+
+        try {
+                nhlog::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 +154,6 @@ main(int argc, char *argv[])
         appTranslator.load("nheko_" + lang, ":/translations");
         app.installTranslator(&appTranslator);
 
-        setupProxy();
-
         MainWindow w;
 
         // Move the MainWindow to the center
@@ -165,7 +163,16 @@ main(int argc, char *argv[])
             !settings.value("user/window/tray", true).toBool())
                 w.show();
 
-        QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize);
+        QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() {
+                w.saveCurrentWindowSize();
+                if (http::v2::client() != nullptr) {
+                        nhlog::net()->info("shutting down all I/O threads & open connections");
+                        http::v2::client()->shutdown();
+                        http::v2::client()->close(true);
+                }
+        });
+
+        nhlog::ui()->info("starting nheko {}", nheko::version);
 
         return app.exec();
 }
diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc
index 250373e4..c104801d 100644
--- a/src/timeline/TimelineItem.cc
+++ b/src/timeline/TimelineItem.cc
@@ -23,6 +23,7 @@
 #include "Avatar.h"
 #include "ChatPage.h"
 #include "Config.h"
+#include "Logging.hpp"
 
 #include "timeline/TimelineItem.h"
 #include "timeline/widgets/AudioItem.h"
@@ -62,9 +63,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(); });
@@ -413,6 +432,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
 void
 TimelineItem::markReceived()
 {
+        isReceived_ = true;
         checkmark_->setText(CHECKMARK);
         checkmark_->setAlignment(Qt::AlignTop);
 
@@ -635,3 +655,19 @@ TimelineItem::addAvatar()
         AvatarProvider::resolve(
           room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); });
 }
+
+void
+TimelineItem::sendReadReceipt() const
+{
+        if (!event_id_.isEmpty())
+                http::v2::client()->read_event(room_id_.toStdString(),
+                                               event_id_.toStdString(),
+                                               [this](mtx::http::RequestErr err) {
+                                                       if (err) {
+                                                               nhlog::net()->warn(
+                                                                 "failed to read_event ({}, {})",
+                                                                 room_id_.toStdString(),
+                                                                 event_id_.toStdString());
+                                                       }
+                                               });
+}
diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc
index 71058d74..e437439e 100644
--- a/src/timeline/TimelineView.cc
+++ b/src/timeline/TimelineView.cc
@@ -23,6 +23,8 @@
 #include "ChatPage.h"
 #include "Config.h"
 #include "FloatingButton.h"
+#include "Logging.hpp"
+#include "Olm.hpp"
 #include "UserSettingsPage.h"
 #include "Utils.h"
 
@@ -100,7 +102,7 @@ TimelineView::TimelineView(const QString &room_id, QWidget *parent)
   , room_id_{room_id}
 {
         init();
-        http::client()->messages(room_id_, "");
+        getMessages();
 }
 
 void
@@ -140,7 +142,7 @@ TimelineView::fetchHistory()
                         return;
 
                 isPaginationInProgress_ = true;
-                http::client()->messages(room_id_, prev_batch_token_);
+                getMessages();
                 paginationTimer_->start(5000);
 
                 return;
@@ -189,18 +191,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;
@@ -239,19 +236,19 @@ TimelineItem *
 TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
                                 TimelineDirection direction)
 {
-        namespace msg     = mtx::events::msg;
-        using AudioEvent  = mtx::events::RoomEvent<msg::Audio>;
-        using EmoteEvent  = mtx::events::RoomEvent<msg::Emote>;
-        using FileEvent   = mtx::events::RoomEvent<msg::File>;
-        using ImageEvent  = mtx::events::RoomEvent<msg::Image>;
-        using NoticeEvent = mtx::events::RoomEvent<msg::Notice>;
-        using TextEvent   = mtx::events::RoomEvent<msg::Text>;
-        using VideoEvent  = mtx::events::RoomEvent<msg::Video>;
+        using namespace mtx::events;
+
+        using AudioEvent  = RoomEvent<msg::Audio>;
+        using EmoteEvent  = RoomEvent<msg::Emote>;
+        using FileEvent   = RoomEvent<msg::File>;
+        using ImageEvent  = RoomEvent<msg::Image>;
+        using NoticeEvent = RoomEvent<msg::Notice>;
+        using TextEvent   = RoomEvent<msg::Text>;
+        using VideoEvent  = RoomEvent<msg::Video>;
 
-        if (mpark::holds_alternative<mtx::events::RedactionEvent<msg::Redaction>>(event)) {
-                auto redaction_event =
-                  mpark::get<mtx::events::RedactionEvent<msg::Redaction>>(event);
-                const auto event_id = QString::fromStdString(redaction_event.redacts);
+        if (mpark::holds_alternative<RedactionEvent<msg::Redaction>>(event)) {
+                auto redaction_event = mpark::get<RedactionEvent<msg::Redaction>>(event);
+                const auto event_id  = QString::fromStdString(redaction_event.redacts);
 
                 QTimer::singleShot(0, this, [event_id, this]() {
                         if (eventIds_.contains(event_id))
@@ -259,35 +256,96 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &
                 });
 
                 return nullptr;
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Audio>>(event)) {
-                auto audio = mpark::get<mtx::events::RoomEvent<msg::Audio>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Audio>>(event)) {
+                auto audio = mpark::get<RoomEvent<msg::Audio>>(event);
                 return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Emote>>(event)) {
-                auto emote = mpark::get<mtx::events::RoomEvent<msg::Emote>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Emote>>(event)) {
+                auto emote = mpark::get<RoomEvent<msg::Emote>>(event);
                 return processMessageEvent<EmoteEvent>(emote, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::File>>(event)) {
-                auto file = mpark::get<mtx::events::RoomEvent<msg::File>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::File>>(event)) {
+                auto file = mpark::get<RoomEvent<msg::File>>(event);
                 return processMessageEvent<FileEvent, FileItem>(file, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Image>>(event)) {
-                auto image = mpark::get<mtx::events::RoomEvent<msg::Image>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Image>>(event)) {
+                auto image = mpark::get<RoomEvent<msg::Image>>(event);
                 return processMessageEvent<ImageEvent, ImageItem>(image, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Notice>>(event)) {
-                auto notice = mpark::get<mtx::events::RoomEvent<msg::Notice>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Notice>>(event)) {
+                auto notice = mpark::get<RoomEvent<msg::Notice>>(event);
                 return processMessageEvent<NoticeEvent>(notice, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Text>>(event)) {
-                auto text = mpark::get<mtx::events::RoomEvent<msg::Text>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Text>>(event)) {
+                auto text = mpark::get<RoomEvent<msg::Text>>(event);
                 return processMessageEvent<TextEvent>(text, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Video>>(event)) {
-                auto video = mpark::get<mtx::events::RoomEvent<msg::Video>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Video>>(event)) {
+                auto video = mpark::get<RoomEvent<msg::Video>>(event);
                 return processMessageEvent<VideoEvent, VideoItem>(video, direction);
-        } else if (mpark::holds_alternative<mtx::events::Sticker>(event)) {
-                return processMessageEvent<mtx::events::Sticker, StickerItem>(
-                  mpark::get<mtx::events::Sticker>(event), direction);
+        } else if (mpark::holds_alternative<Sticker>(event)) {
+                return processMessageEvent<Sticker, StickerItem>(mpark::get<Sticker>(event),
+                                                                 direction);
+        } else if (mpark::holds_alternative<EncryptedEvent<msg::Encrypted>>(event)) {
+                auto decrypted =
+                  parseEncryptedEvent(mpark::get<EncryptedEvent<msg::Encrypted>>(event));
+                return parseMessageEvent(decrypted, direction);
+        } else if (mpark::holds_alternative<StateEvent<state::Encryption>>(event)) {
+                try {
+                        cache::client()->setEncryptedRoom(room_id_.toStdString());
+                } catch (const lmdb::error &e) {
+                        nhlog::db()->critical("failed to save room {} as encrypted",
+                                              room_id_.toStdString());
+                }
         }
 
         return nullptr;
 }
 
+TimelineEvent
+TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
+{
+        MegolmSessionIndex index;
+        index.room_id    = room_id_.toStdString();
+        index.session_id = e.content.session_id;
+        index.sender_key = e.content.sender_key;
+
+        mtx::events::RoomEvent<mtx::events::msg::Text> dummy;
+        dummy.origin_server_ts = e.origin_server_ts;
+        dummy.event_id         = e.event_id;
+        dummy.sender           = e.sender;
+        dummy.content.body     = "-- Encrypted Event (No keys found for decryption) --";
+
+        if (!cache::client()->inboundMegolmSessionExists(index)) {
+                nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
+                                      index.room_id,
+                                      index.session_id,
+                                      e.sender);
+                // TODO: request megolm session_id & session_key from the sender.
+                return dummy;
+        }
+
+        auto session = cache::client()->getInboundMegolmSession(index);
+        auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
+
+        const auto msg_str = std::string((char *)res.data.data(), res.data.size());
+
+        // Add missing fields for the event.
+        json body                = json::parse(msg_str);
+        body["event_id"]         = e.event_id;
+        body["sender"]           = e.sender;
+        body["origin_server_ts"] = e.origin_server_ts;
+        body["unsigned"]         = e.unsigned_data;
+
+        nhlog::crypto()->info("decrypted data: \n {}", body.dump(2));
+
+        json event_array = json::array();
+        event_array.push_back(body);
+
+        std::vector<TimelineEvent> events;
+        mtx::responses::utils::parse_timeline_events(event_array, events);
+
+        if (events.size() == 1)
+                return events.at(0);
+
+        dummy.content.body = "-- Encrypted Event (Unknown event type) --";
+        return dummy;
+}
+
 void
 TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
 {
@@ -427,10 +485,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)),
@@ -443,6 +501,27 @@ TimelineView::init()
 }
 
 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) {
+                          nhlog::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)
 {
         if (direction == TimelineDirection::Bottom)
@@ -513,8 +592,9 @@ 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)
 {
+        nhlog::ui()->info("[{}] message was received by the server", txn_id);
         if (!pending_msgs_.isEmpty() &&
             pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
                 auto msg     = pending_msgs_.dequeue();
@@ -522,11 +602,18 @@ TimelineView::updatePendingMessage(int txn_id, QString event_id)
 
                 if (msg.widget) {
                         msg.widget->setEventId(event_id);
-                        msg.widget->markReceived();
                         eventIds_[event_id] = msg.widget;
-                }
 
-                pending_sent_msgs_.append(msg);
+                        // If the response comes after we have received the event from sync
+                        // we've already marked the widget as received.
+                        if (!msg.widget->isReceived()) {
+                                msg.widget->markReceived();
+                                pending_sent_msgs_.append(msg);
+                        }
+                } else {
+                        nhlog::ui()->warn("[{}] received message response for invalid widget",
+                                          txn_id);
+                }
         }
 
         sendNextPendingMessage();
@@ -540,16 +627,28 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
         TimelineItem *view_item =
           new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_);
 
+        PendingMessage message;
+        message.ty     = ty;
+        message.txn_id = http::v2::client()->generate_txn_id();
+        message.body   = body;
+        message.widget = view_item;
+
+        try {
+                message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString());
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical("failed to check encryption status of room {}", e.what());
+                view_item->deleteLater();
+
+                // TODO: Send a notification to the user.
+
+                return;
+        }
+
         addTimelineItem(view_item);
 
         lastMessageDirection_ = TimelineDirection::Bottom;
 
-        QApplication::processEvents();
-
         saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
-
-        int txn_id = http::client()->incrementTransactionId();
-        PendingMessage message(ty, txn_id, body, "", "", -1, "", view_item);
         handleNewUserMessage(message);
 }
 
@@ -567,19 +666,98 @@ TimelineView::sendNextPendingMessage()
         if (pending_msgs_.size() == 0)
                 return;
 
+        using namespace mtx::events;
+
         PendingMessage &m = pending_msgs_.head();
+
+        nhlog::ui()->info("[{}] sending next queued message", m.txn_id);
+
+        if (m.is_encrypted) {
+                prepareEncryptedMessage(std::move(m));
+                nhlog::ui()->info("[{}] sending encrypted event", m.txn_id);
+                return;
+        }
+
         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: {
+                http::v2::client()->send_room_message<msg::Audio, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  toRoomMessage<msg::Audio>(m),
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::Image: {
+                http::v2::client()->send_room_message<msg::Image, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  toRoomMessage<msg::Image>(m),
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::Video: {
+                http::v2::client()->send_room_message<msg::Video, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  toRoomMessage<msg::Video>(m),
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::File: {
+                http::v2::client()->send_room_message<msg::File, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  toRoomMessage<msg::File>(m),
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
                 break;
+        }
+        case mtx::events::MessageType::Text: {
+                http::v2::client()->send_room_message<msg::Text, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  toRoomMessage<msg::Text>(m),
+                  std::bind(&TimelineView::sendRoomMessageHandler,
+                            this,
+                            m.txn_id,
+                            std::placeholders::_1,
+                            std::placeholders::_2));
+
+                break;
+        }
+        case mtx::events::MessageType::Emote: {
+                http::v2::client()->send_room_message<msg::Emote, EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  m.txn_id,
+                  toRoomMessage<msg::Emote>(m),
+                  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);
+                nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString());
                 break;
         }
 }
@@ -593,7 +771,7 @@ TimelineView::notifyForLastEvent()
         if (lastTimelineItem)
                 emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
         else
-                qWarning() << "Cast to TimelineView failed" << room_id_;
+                nhlog::ui()->warn("cast to TimelineView failed: {}", room_id_.toStdString());
 }
 
 void
@@ -606,51 +784,51 @@ 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);
 
                         if (pending_sent_msgs_.isEmpty())
                                 sendNextPendingMessage();
 
-                        return;
+                        nhlog::ui()->info("[{}] removed message with sync", txn_id);
                 }
         }
         for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
-                if (QString::number(it->txn_id) == txnid) {
-                        int index = std::distance(pending_msgs_.begin(), it);
-                        pending_msgs_.removeAt(index);
+                if (it->txn_id == txn_id) {
+                        if (it->widget)
+                                it->widget->markReceived();
+
+                        nhlog::ui()->info("[{}] received sync before message response", txn_id);
                         return;
                 }
         }
 }
 
 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 +851,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) {
+                                                               nhlog::net()->warn(
+                                                                 "failed to read event ({}, {})",
+                                                                 room_id_.toStdString(),
+                                                                 eventId.toStdString());
+                                                       }
+                                               });
 }
 
 QString
@@ -743,7 +930,8 @@ void
 TimelineView::removeEvent(const QString &event_id)
 {
         if (!eventIds_.contains(event_id)) {
-                qWarning() << "unknown event_id couldn't be removed:" << event_id;
+                nhlog::ui()->warn("cannot remove widget with unknown event_id: {}",
+                                  event_id.toStdString());
                 return;
         }
 
@@ -860,3 +1048,332 @@ 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) {
+                const int status_code = static_cast<int>(err->status_code);
+                nhlog::net()->warn("[{}] failed to send message: {} {}",
+                                   txn_id,
+                                   err->matrix_error.error,
+                                   status_code);
+                emit messageFailed(txn_id);
+                return;
+        }
+
+        emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string()));
+}
+
+template<>
+mtx::events::msg::Audio
+toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m)
+{
+        mtx::events::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();
+        return audio;
+}
+
+template<>
+mtx::events::msg::Image
+toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m)
+{
+        mtx::events::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();
+        return image;
+}
+
+template<>
+mtx::events::msg::Video
+toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m)
+{
+        mtx::events::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();
+        return video;
+}
+
+template<>
+mtx::events::msg::Emote
+toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m)
+{
+        mtx::events::msg::Emote emote;
+        emote.body = m.body.toStdString();
+        return emote;
+}
+
+template<>
+mtx::events::msg::File
+toRoomMessage<mtx::events::msg::File>(const PendingMessage &m)
+{
+        mtx::events::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();
+        return file;
+}
+
+template<>
+mtx::events::msg::Text
+toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m)
+{
+        mtx::events::msg::Text text;
+        text.body = m.body.toStdString();
+        return text;
+}
+
+void
+TimelineView::prepareEncryptedMessage(const PendingMessage &msg)
+{
+        const auto room_id = room_id_.toStdString();
+
+        using namespace mtx::events;
+        using namespace mtx::identifiers;
+
+        json content;
+
+        // Serialize the message to the plaintext that will be encrypted.
+        switch (msg.ty) {
+        case MessageType::Audio: {
+                content = json(toRoomMessage<msg::Audio>(msg));
+                break;
+        }
+        case MessageType::Emote: {
+                content = json(toRoomMessage<msg::Emote>(msg));
+                break;
+        }
+        case MessageType::File: {
+                content = json(toRoomMessage<msg::File>(msg));
+                break;
+        }
+        case MessageType::Image: {
+                content = json(toRoomMessage<msg::Image>(msg));
+                break;
+        }
+        case MessageType::Text: {
+                content = json(toRoomMessage<msg::Text>(msg));
+                break;
+        }
+        case MessageType::Video: {
+                content = json(toRoomMessage<msg::Video>(msg));
+                break;
+        }
+        default:
+                break;
+        }
+
+        json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
+
+        try {
+                // Check if we have already an outbound megolm session then we can use.
+                if (cache::client()->outboundMegolmSessionExists(room_id)) {
+                        auto data = olm::encrypt_group_message(
+                          room_id, http::v2::client()->device_id(), doc.dump());
+
+                        http::v2::client()
+                          ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+                            room_id,
+                            msg.txn_id,
+                            data,
+                            std::bind(&TimelineView::sendRoomMessageHandler,
+                                      this,
+                                      msg.txn_id,
+                                      std::placeholders::_1,
+                                      std::placeholders::_2));
+                        return;
+                }
+
+                nhlog::ui()->info("creating new outbound megolm session");
+
+                // Create a new outbound megolm session.
+                auto outbound_session  = olm::client()->init_outbound_group_session();
+                const auto session_id  = mtx::crypto::session_id(outbound_session.get());
+                const auto session_key = mtx::crypto::session_key(outbound_session.get());
+
+                // TODO: needs to be moved in the lib.
+                auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
+                                           {"room_id", room_id},
+                                           {"session_id", session_id},
+                                           {"session_key", session_key}};
+
+                // Saving the new megolm session.
+                // TODO: Maybe it's too early to save.
+                OutboundGroupSessionData session_data;
+                session_data.session_id    = session_id;
+                session_data.session_key   = session_key;
+                session_data.message_index = 0; // TODO Update me
+                cache::client()->saveOutboundMegolmSession(
+                  room_id, session_data, std::move(outbound_session));
+
+                const auto members = cache::client()->roomMembers(room_id);
+                nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
+
+                auto keeper = std::make_shared<StateKeeper>(
+                  [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() {
+                          try {
+                                  auto data = olm::encrypt_group_message(
+                                    room_id, http::v2::client()->device_id(), doc.dump());
+
+                                  http::v2::client()
+                                    ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+                                      room_id,
+                                      txn_id,
+                                      data,
+                                      std::bind(&TimelineView::sendRoomMessageHandler,
+                                                this,
+                                                txn_id,
+                                                std::placeholders::_1,
+                                                std::placeholders::_2));
+
+                          } catch (const lmdb::error &e) {
+                                  nhlog::db()->critical(
+                                    "failed to save megolm outbound session: {}", e.what());
+                          }
+                  });
+
+                mtx::requests::QueryKeys req;
+                for (const auto &member : members)
+                        req.device_keys[member] = {};
+
+                http::v2::client()->query_keys(
+                  req,
+                  [keeper = std::move(keeper), megolm_payload, this](
+                    const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::net()->warn("failed to query device keys: {} {}",
+                                                     err->matrix_error.error,
+                                                     static_cast<int>(err->status_code));
+                                  // TODO: Mark the event as failed. Communicate with the UI.
+                                  return;
+                          }
+
+                          for (const auto &entry : res.device_keys) {
+                                  for (const auto &dev : entry.second) {
+                                          nhlog::net()->info("received device {}", dev.first);
+
+                                          const auto device_keys = dev.second.keys;
+                                          const auto curveKey    = "curve25519:" + dev.first;
+                                          const auto edKey       = "ed25519:" + dev.first;
+
+                                          if ((device_keys.find(curveKey) == device_keys.end()) ||
+                                              (device_keys.find(edKey) == device_keys.end())) {
+                                                  nhlog::net()->info(
+                                                    "ignoring malformed keys for device {}",
+                                                    dev.first);
+                                                  continue;
+                                          }
+
+                                          DevicePublicKeys pks;
+                                          pks.ed25519    = device_keys.at(edKey);
+                                          pks.curve25519 = device_keys.at(curveKey);
+
+                                          // Validate signatures
+                                          for (const auto &algo : dev.second.keys) {
+                                                  nhlog::net()->info(
+                                                    "dev keys {} {}", algo.first, algo.second);
+                                          }
+
+                                          auto room_key =
+                                            olm::client()
+                                              ->create_room_key_event(UserId(dev.second.user_id),
+                                                                      pks.ed25519,
+                                                                      megolm_payload)
+                                              .dump();
+
+                                          http::v2::client()->claim_keys(
+                                            dev.second.user_id,
+                                            {dev.second.device_id},
+                                            std::bind(&TimelineView::handleClaimedKeys,
+                                                      this,
+                                                      keeper,
+                                                      room_key,
+                                                      pks,
+                                                      dev.second.user_id,
+                                                      dev.second.device_id,
+                                                      std::placeholders::_1,
+                                                      std::placeholders::_2));
+                                  }
+                          }
+                  });
+
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical(
+                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
+                return;
+        }
+}
+
+void
+TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+                                const std::string &room_key,
+                                const DevicePublicKeys &pks,
+                                const std::string &user_id,
+                                const std::string &device_id,
+                                const mtx::responses::ClaimKeys &res,
+                                mtx::http::RequestErr err)
+{
+        if (err) {
+                nhlog::net()->warn("claim keys error: {}", err->matrix_error.error);
+                return;
+        }
+
+        nhlog::net()->info("claimed keys for {} - {}", user_id, device_id);
+
+        if (res.one_time_keys.size() == 0) {
+                nhlog::net()->info("no one-time keys found for device_id: {}", device_id);
+                return;
+        }
+
+        if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
+                nhlog::net()->info(
+                  "no one-time keys found in device_id {} for the user {}", device_id, user_id);
+                return;
+        }
+
+        auto retrieved_devices = res.one_time_keys.at(user_id);
+
+        for (const auto &rd : retrieved_devices) {
+                nhlog::net()->info("{} : \n {}", rd.first, rd.second.dump(2));
+
+                // TODO: Verify signatures
+                auto otk    = rd.second.begin()->at("key");
+                auto id_key = pks.curve25519;
+
+                auto s = olm::client()->create_outbound_session(id_key, otk);
+
+                auto device_msg =
+                  olm::client()->create_olm_encrypted_content(s.get(), room_key, pks.curve25519);
+
+                try {
+                        cache::client()->saveOlmSession(id_key, std::move(s));
+                } catch (const lmdb::error &e) {
+                        nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
+                } catch (const mtx::crypto::olm_exception &e) {
+                        nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
+                                                  e.what());
+                }
+
+                json body{{"messages", {{user_id, {{device_id, device_msg}}}}}};
+
+                http::v2::client()->send_to_device(
+                  "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::net()->warn("failed to send "
+                                                     "send_to_device "
+                                                     "message: {}",
+                                                     err->matrix_error.error);
+                          }
+                  });
+        }
+}
diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc
index b7ce53ae..7ea1ee4a 100644
--- a/src/timeline/TimelineViewManager.cc
+++ b/src/timeline/TimelineViewManager.cc
@@ -18,12 +18,10 @@
 #include <random>
 
 #include <QApplication>
-#include <QDebug>
 #include <QFileInfo>
 #include <QSettings>
 
-#include "MatrixClient.h"
-
+#include "Logging.hpp"
 #include "timeline/TimelineView.h"
 #include "timeline/TimelineViewManager.h"
 #include "timeline/widgets/AudioItem.h"
@@ -35,42 +33,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
@@ -105,7 +76,7 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
                                        uint64_t size)
 {
         if (!timelineViewExists(roomid)) {
-                qDebug() << "Cannot send m.image message to a non-managed view";
+                nhlog::ui()->warn("Cannot send m.image message to a non-managed view");
                 return;
         }
 
@@ -122,7 +93,7 @@ TimelineViewManager::queueFileMessage(const QString &roomid,
                                       uint64_t size)
 {
         if (!timelineViewExists(roomid)) {
-                qDebug() << "Cannot send m.file message to a non-managed view";
+                nhlog::ui()->warn("cannot send m.file message to a non-managed view");
                 return;
         }
 
@@ -139,7 +110,7 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
                                        uint64_t size)
 {
         if (!timelineViewExists(roomid)) {
-                qDebug() << "Cannot send m.audio message to a non-managed view";
+                nhlog::ui()->warn("cannot send m.audio message to a non-managed view");
                 return;
         }
 
@@ -156,7 +127,7 @@ TimelineViewManager::queueVideoMessage(const QString &roomid,
                                        uint64_t size)
 {
         if (!timelineViewExists(roomid)) {
-                qDebug() << "Cannot send m.video message to a non-managed view";
+                nhlog::ui()->warn("cannot send m.video message to a non-managed view");
                 return;
         }
 
@@ -227,7 +198,8 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
                 auto roomid = QString::fromStdString(room.first);
 
                 if (!timelineViewExists(roomid)) {
-                        qDebug() << "Ignoring event from unknown room" << roomid;
+                        nhlog::ui()->warn("ignoring event from unknown room: {}",
+                                          roomid.toStdString());
                         continue;
                 }
 
@@ -241,7 +213,8 @@ void
 TimelineViewManager::setHistoryView(const QString &room_id)
 {
         if (!timelineViewExists(room_id)) {
-                qDebug() << "Room ID from RoomList is not present in ViewManager" << room_id;
+                nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}",
+                                  room_id.toStdString());
                 return;
         }
 
diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cc
index 65ca401b..7cbbed28 100644
--- a/src/timeline/widgets/AudioItem.cc
+++ b/src/timeline/widgets/AudioItem.cc
@@ -16,13 +16,13 @@
  */
 
 #include <QBrush>
-#include <QDebug>
 #include <QDesktopServices>
 #include <QFile>
 #include <QFileDialog>
 #include <QPainter>
 #include <QPixmap>
 
+#include "Logging.hpp"
 #include "MatrixClient.h"
 #include "Utils.h"
 
@@ -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,20 @@ 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) {
+                                  nhlog::net()->info("failed to retrieve m.audio content: {}",
+                                                     url_.toString().toStdString());
+                                  return;
+                          }
+
+                          emit fileDownloadedCb(QByteArray(data.data(), data.size()));
+                  });
         }
 }
 
@@ -151,8 +148,8 @@ AudioItem::fileDownloaded(const QByteArray &data)
 
                 file.write(data);
                 file.close();
-        } catch (const std::exception &ex) {
-                qDebug() << "Error while saving file to:" << ex.what();
+        } catch (const std::exception &e) {
+                nhlog::ui()->warn("error while saving file: {}", e.what());
         }
 }
 
diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cc
index f3906a04..4ce4d256 100644
--- a/src/timeline/widgets/FileItem.cc
+++ b/src/timeline/widgets/FileItem.cc
@@ -16,13 +16,13 @@
  */
 
 #include <QBrush>
-#include <QDebug>
 #include <QDesktopServices>
 #include <QFile>
 #include <QFileDialog>
 #include <QPainter>
 #include <QPixmap>
 
+#include "Logging.hpp"
 #include "MatrixClient.h"
 #include "Utils.h"
 
@@ -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))
+                nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString());
 }
 
 QSize
@@ -115,14 +114,20 @@ 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) {
+                                  nhlog::ui()->warn("failed to retrieve m.file content: {}",
+                                                    url_.toString().toStdString());
+                                  return;
+                          }
+
+                          emit fileDownloadedCb(QByteArray(data.data(), data.size()));
+                  });
         } else {
                 openUrl();
         }
@@ -139,8 +144,8 @@ FileItem::fileDownloaded(const QByteArray &data)
 
                 file.write(data);
                 file.close();
-        } catch (const std::exception &ex) {
-                qDebug() << "Error while saving file to:" << ex.what();
+        } catch (const std::exception &e) {
+                nhlog::ui()->warn("Error while saving file to: {}", e.what());
         }
 }
 
diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cc
index 66cd31ab..bf1c05d6 100644
--- a/src/timeline/widgets/ImageItem.cc
+++ b/src/timeline/widgets/ImageItem.cc
@@ -16,7 +16,6 @@
  */
 
 #include <QBrush>
-#include <QDebug>
 #include <QDesktopServices>
 #include <QFileDialog>
 #include <QFileInfo>
@@ -25,42 +24,71 @@
 #include <QUuid>
 
 #include "Config.h"
+#include "Logging.hpp"
 #include "MatrixClient.h"
 #include "Utils.h"
 #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) {
+                                                     nhlog::net()->warn(
+                                                       "failed to retrieve image {}: {} {}",
+                                                       url.toString().toStdString(),
+                                                       err->matrix_error.error,
+                                                       static_cast<int>(err->status_code));
+                                                     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 &e) {
+                nhlog::ui()->warn("Error while saving file to: {}", e.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 +97,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 +106,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))
+                nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString());
 }
 
 QSize
@@ -231,23 +242,22 @@ 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();
-                        }
-                });
+        const auto url = url_.toString().toStdString();
+
+        http::v2::client()->download(
+          url,
+          [this, filename, url](const std::string &data,
+                                const std::string &,
+                                const std::string &,
+                                mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
+                                             url,
+                                             err->matrix_error.error,
+                                             static_cast<int>(err->status_code));
+                          return;
+                  }
+
+                  emit imageSaved(filename, QByteArray(data.data(), data.size()));
+          });
 }
diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cc
index f5bcfd6e..34d963a9 100644
--- a/src/timeline/widgets/VideoItem.cc
+++ b/src/timeline/widgets/VideoItem.cc
@@ -15,7 +15,6 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <QDebug>
 #include <QLabel>
 #include <QVBoxLayout>
 
@@ -27,15 +26,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)