diff --git a/.ci/install.sh b/.ci/install.sh
index 0942af62af5d29ebd062e31c3cb6e5dd1d7fe43c..2c7c71e3f9309da4c1b76f6ef5d5f1f7c034da10 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -31,8 +31,8 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
         QT_PKG="59"
     fi
 
-    wget https://cmake.org/files/v3.12/cmake-3.12.2-Linux-x86_64.sh
-    sudo sh cmake-3.12.2-Linux-x86_64.sh  --skip-license  --prefix=/usr/local
+    wget https://cmake.org/files/v3.15/cmake-3.15.5-Linux-x86_64.sh
+    sudo sh cmake-3.15.5-Linux-x86_64.sh  --skip-license  --prefix=/usr/local
 
     mkdir -p build-libsodium
     ( cd build-libsodium
diff --git a/.ci/script.sh b/.ci/script.sh
index ac6bfed60fbf2bd0ab9cd8de506b415af1b373c7..06536278e3b8dbea4b4ef14166712a76966c5394 100755
--- a/.ci/script.sh
+++ b/.ci/script.sh
@@ -13,6 +13,9 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
 
     sudo update-alternatives --set gcc "/usr/bin/${C_COMPILER}"
     sudo update-alternatives --set g++ "/usr/bin/${CXX_COMPILER}"
+
+    export PATH="/usr/local/bin/:${PATH}"
+    cmake --version
 fi
 
 if [ "$TRAVIS_OS_NAME" = "linux" ]; then
@@ -35,7 +38,8 @@ cmake --build .deps
 # Build nheko
 cmake -GNinja -H. -Bbuild \
     -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-    -DCMAKE_INSTALL_PREFIX=.deps/usr
+    -DCMAKE_INSTALL_PREFIX=.deps/usr \
+    -DBUILD_SHARED_LIBS=ON # weird workaround, as the boost 1.70 cmake files seem to be broken?
 cmake --build build
 
 if [ "$TRAVIS_OS_NAME" = "osx" ]; then
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e07df88d8bf3c3f24b5fcacd0ad78de1f60a07c6..67a1dfb09c1074ddcdc27bbfb286669e809a9b79 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -259,7 +259,7 @@ include(FeatureSummary)
 set(Boost_USE_STATIC_LIBS OFF)
 set(Boost_USE_STATIC_RUNTIME OFF)
 set(Boost_USE_MULTITHREADED ON)
-find_package(Boost 1.66 REQUIRED
+find_package(Boost 1.70 REQUIRED
              COMPONENTS atomic
                         chrono
                         date_time
@@ -365,6 +365,7 @@ qt5_wrap_cpp(MOC_HEADERS
     src/CommunitiesList.h
     src/LoginPage.h
     src/MainWindow.h
+    src/MxcImageProvider.h
     src/InviteeItem.h
     src/QuickSwitcher.h
     src/RegisterPage.h
diff --git a/README.md b/README.md
index efa37e89eb29d5d9592173488d1a8e11d3db1252..0380a90a3906cf9e8fc675b9fac3bd6f276f29da 100644
--- a/README.md
+++ b/README.md
@@ -92,11 +92,11 @@ sudo port install nheko
 - Qt5 (5.8 or greater). Qt 5.7 adds support for color font rendering with
   Freetype, which is essential to properly support emoji, 5.8 adds some features
   to make interopability with Qml easier.
-- CMake 3.1 or greater.
+- CMake 3.15 or greater. (Lower version may work, but may break boost linking)
 - [mtxclient](https://github.com/Nheko-Reborn/mtxclient)
 - [LMDB](https://symas.com/lightning-memory-mapped-database/)
 - [cmark](https://github.com/commonmark/cmark)
-- Boost 1.66 or greater.
+- Boost 1.70 or greater.
 - [libolm](https://git.matrix.org/git/olm)
 - [libsodium](https://github.com/jedisct1/libsodium)
 - [spdlog](https://github.com/gabime/spdlog)
diff --git a/appveyor.yml b/appveyor.yml
index 08251174aaf932cee1e6818a8603ec21dffca53e..8572418f38d51978aad91efa38f5540547ec1a13 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -34,6 +34,7 @@ install:
             lmdb:%PLATFORM%-windows
             openssl:%PLATFORM%-windows
             zlib:%PLATFORM%-windows
+    - vcpkg upgrade --no-dry-run
 
 build_script:
     # VERSION format:     branch-master/branch-1.2
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
index d0a715e058b4cb441a088d285b860457a2442517..0da4a67151f4b75e23a92a16b2f1b9210e874302 100644
--- a/deps/CMakeLists.txt
+++ b/deps/CMakeLists.txt
@@ -33,23 +33,23 @@ option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json." ${USE_BUNDLE
 option(MTX_STATIC "Compile / link bundled mtx client statically" OFF)
 
 if(USE_BUNDLED_BOOST)
-  # bundled boost is 1.68, which requires CMake 3.12 or greater.
-  cmake_minimum_required(VERSION 3.12)
+  # bundled boost is 1.70, which requires CMake 3.15 or greater.
+  cmake_minimum_required(VERSION 3.15)
 endif()
 
 include(ExternalProject)
 
 set(BOOST_URL
-    https://dl.bintray.com/boostorg/release/1.69.0/source/boost_1_69_0.tar.bz2)
+    https://dl.bintray.com/boostorg/release/1.70.0/source/boost_1_70_0.tar.bz2)
 set(BOOST_SHA256
-    8f32d4617390d1c2d16f26a27ab60d97807b35440d45891fa340fc2648b04406)
+    430ae8354789de4fd19ee52f3b1f739e1fba576f0aded0897c3c2bc00fb38778)
 
 set(
   MTXCLIENT_URL
-  https://github.com/Nheko-Reborn/mtxclient/archive/6eee767cc25a9db9f125843e584656cde1ebb6c5.tar.gz
+  https://github.com/Nheko-Reborn/mtxclient/archive/64182a84e35378113f7d3a80f3073894416480e7.zip
   )
 set(MTXCLIENT_HASH
-  72fe77da4fed98b3cf069299f66092c820c900359a27ec26070175f9ad208a03)
+    c9973501920046f04c72983472451736343d00e7a40f4d4a12181191093a5fab)
 set(
   TWEENY_URL
   https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz
diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts
index 879551bd04cc1ca4143c3702a943d044f301c138..59c6dffd85c5cc196f6c2ec2214279f0f3fd521a 100644
--- a/resources/langs/nheko_de.ts
+++ b/resources/langs/nheko_de.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
-        <translation>Hochladen des Bildes fehlgeschlagen. Bitte versuche es erneut.</translation>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
+        <translation>Medienupload fehlgeschlagen. Bitte versuche es erneut.</translation>
     </message>
     <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation>Hochladen der Datei fehlgeschlagen. Bitte versuche es erneut.</translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation>Hochladen der Audiodatei fehlgeschlagen. Bitte versuche es erneut.</translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation>Hochladen der Videodatei fehlgeschlagen. Bitte versuche es erneut.</translation>
-    </message>
-    <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.</translation>
     </message>
@@ -194,6 +179,19 @@
         <translation>OK</translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation>gelöscht</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation>Verschlüsselung aktiviert</translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Raum suchen…</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation>gelöscht</translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation>Versende Datei</translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Schreibe eine Nachricht…</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation>Emoji</translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Datei auswählen</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation>-- verschlüsselter Event (keine Schlüssel zur Entschlüsselung gefunden) --</translation>
@@ -423,10 +413,30 @@
         <translation>-- verschlüsselter Event (Unbekannter Eventtyp) --</translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation>Nachricht zurückziehen fehlgeschlagen: %1</translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation>Bild speichern</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation>Video speichern</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation>Audiodatei speichern</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation>Datei speichern</translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation>Kein Raum geöffnet</translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation>Bild speichern</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation>Video speichern</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation>Audiodatei speichern</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation>Datei speichern</translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts
index e9c70da00221ff50ac1f574d8ce8719049f22071..fe65785bdaf2a0e1b33be757ca7b01977fb366cd 100644
--- a/resources/langs/nheko_el.ts
+++ b/resources/langs/nheko_el.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -194,6 +179,19 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Αναζήτηση συνομιλίας...</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Γράψε ένα μήνυμα...</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Διάλεξε ένα αρχείο</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished"></translation>
@@ -423,10 +413,30 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">Αποθήκευση Εικόνας</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">Αποθήκευση Εικόνας</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts
index cb2ef1c7cdee27d3246a7326eaa5ef1cad4066ed..49ea7439b3de103141e7e7e58b59e513e66a9cd9 100644
--- a/resources/langs/nheko_en.ts
+++ b/resources/langs/nheko_en.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
-        <translation>Failed to upload image. Please try again.</translation>
-    </message>
-    <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation>Failed to upload file. Please try again.</translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation>Failed to upload audio. Please try again.</translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation>Failed to upload video. Please try again.</translation>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Failed to restore OLM account. Please login again.</translation>
     </message>
@@ -194,6 +179,19 @@
         <translation>OK</translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Search for a room…</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation>Send a file</translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Write a message…</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation>Emoji</translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Select a file</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished">-- Encrypted Event (No keys found for decryption) --</translation>
@@ -423,10 +413,30 @@
         <translation type="unfinished">-- Encrypted Event (Unknown event type) --</translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished">Message redaction failed: %1</translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">Save image</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">Save image</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts
index 76bf706407030fb73d077ce311ab6f795cf544ec..4bb20e3098f0a40afa07d6d26ffc5e629812549f 100644
--- a/resources/langs/nheko_fi.ts
+++ b/resources/langs/nheko_fi.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
-        <translation>Kuvan lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation>
-    </message>
-    <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation>Tiedoston lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation>Äänitiedoston lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation>Videon lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen.</translation>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation>
     </message>
@@ -194,6 +179,19 @@
         <translation>OK</translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Etsi huonetta…</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation>Lähetä tiedosto</translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Kirjoita viesti…</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation>Emoji</translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Valitse tiedosto</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished">-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation>
@@ -423,10 +413,30 @@
         <translation type="unfinished">-- Salattu viesti (tuntematon viestityyppi) --</translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished">Viestin poisto epäonnistui: %1</translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">Tallenna kuva</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">Tallenna kuva</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts
index 30ff85994096eb682064856633976ca7a91f2570..8ef2226822ed8831b683b970782caf68bde918cf 100644
--- a/resources/langs/nheko_fr.ts
+++ b/resources/langs/nheko_fr.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -194,6 +179,19 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Chercher un salon…</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -355,13 +345,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Écrivez un message...</translation>
     </message>
@@ -376,7 +366,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Sélectionnez un fichier</translation>
     </message>
@@ -394,7 +384,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished"></translation>
@@ -424,10 +414,30 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">Enregistrer l&apos;image</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -475,29 +485,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">Enregistrer l&apos;image</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts
index 1c8a83c0205e00257826fa80dc9673b682df7b8b..aaeae41c242c3a73e928cf187f0970582f69f88e 100644
--- a/resources/langs/nheko_nl.ts
+++ b/resources/langs/nheko_nl.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -194,6 +179,19 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Zoek een kamer...</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Typ een bericht...</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Kies een bestand</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished"></translation>
@@ -423,10 +413,30 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">Afbeelding opslaan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">Afbeelding opslaan</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts
index 6c3b2abd4fea6f37f161437bf5987bd8c3eedca3..b7c3878d9fc97dfcd25a5da6258fc2f0b7fd7d77 100644
--- a/resources/langs/nheko_pl.ts
+++ b/resources/langs/nheko_pl.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
-        <translation>Nie udało się wysłać obrazu. Spróbuj ponownie.</translation>
-    </message>
-    <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation>Nie udało się wysłać pliku. Spróbuj ponownie.</translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation>Nie udało się wysłać pliku dźwiękowego. Spróbuj ponownie.</translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation>Nie udało się wysłać filmu. Spróbuj ponownie.</translation>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Nie udało się przywrócić konta OLM. Spróbuj zalogować się ponownie.</translation>
     </message>
@@ -194,6 +179,19 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Wyszukaj pokoju…</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation>Wyślij plik</translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Napisz wiadomość…</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation>Emoji</translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Wybierz plik</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished"></translation>
@@ -423,10 +413,30 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished">Redagowanie wiadomości nie powiodło się: %1</translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">Zapisz obraz</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">Zapisz obraz</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts
index d5544cf84688fff70c65b05b1b82bc4067184ea7..3069cdadb3347b81f9199e33e65e13519825015b 100644
--- a/resources/langs/nheko_ru.ts
+++ b/resources/langs/nheko_ru.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
-        <translation>Не удалось загрузить изображение. Пожалуйста, попробуйте еще раз.</translation>
-    </message>
-    <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation>Не удалось загрузить файл. Пожалуйста, попробуйте еще раз.</translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation>Не удалось загрузить аудио. Пожалуйста, попробуйте еще раз.</translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation>Не удалось загрузить видео. Пожалуйста, попробуйте еще раз.</translation>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.</translation>
     </message>
@@ -194,6 +179,19 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>Поиск комнаты...</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation>Отправить файл</translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>Написать сообщение...</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>Выберите файл</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished"></translation>
@@ -423,10 +413,30 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished">Ошибка редактирования сообщения: %1</translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">Сохранить изображение</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">Сохранить изображение</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts
index 57f49d431541f8f46a34c5c983c2161cfcacd58e..31ca068cdfb9d48e64d89a575ad3d34719c453ed 100644
--- a/resources/langs/nheko_zh_CN.ts
+++ b/resources/langs/nheko_zh_CN.ts
@@ -4,27 +4,12 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. Please try again.</source>
-        <translation>上传图像失败。请重试。</translation>
-    </message>
-    <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation>上传文件失败,请重试。</translation>
-    </message>
-    <message>
-        <location line="+43"/>
-        <source>Failed to upload audio. Please try again.</source>
-        <translation>上传音频失败。请重试。</translation>
-    </message>
-    <message>
-        <location line="+42"/>
-        <source>Failed to upload video. Please try again.</source>
-        <translation>上传视频失败。请重试。</translation>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+393"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>恢复 OLM 账户失败。请重新登录。</translation>
     </message>
@@ -194,6 +179,19 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+43"/>
+        <source>redacted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Placeholder</name>
     <message>
@@ -210,14 +208,6 @@
         <translation>寻找一个聊天室...</translation>
     </message>
 </context>
-<context>
-    <name>Redacted</name>
-    <message>
-        <location filename="../qml/delegates/Redacted.qml" line="+5"/>
-        <source>redacted</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>RegisterPage</name>
     <message>
@@ -354,13 +344,13 @@
 <context>
     <name>TextInputWidget</name>
     <message>
-        <location filename="../../src/TextInputWidget.cpp" line="+507"/>
+        <location filename="../../src/TextInputWidget.cpp" line="+502"/>
         <source>Send a file</source>
         <translation>发送一个文件</translation>
     </message>
     <message>
         <location line="+13"/>
-        <location filename="../../src/TextInputWidget.h" line="+164"/>
+        <location filename="../../src/TextInputWidget.h" line="+161"/>
         <source>Write a message...</source>
         <translation>写一条消息...</translation>
     </message>
@@ -375,7 +365,7 @@
         <translation></translation>
     </message>
     <message>
-        <location line="+75"/>
+        <location line="+72"/>
         <source>Select a file</source>
         <translation>选择一个文件</translation>
     </message>
@@ -393,7 +383,7 @@
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+780"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+835"/>
         <source>-- Encrypted Event (No keys found for decryption) --</source>
         <comment>Placeholder, when the message was not decrypted yet or can&apos;t be decrypted</comment>
         <translation type="unfinished"></translation>
@@ -423,10 +413,30 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+50"/>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished">删除消息失败:%1</translation>
     </message>
+    <message>
+        <location line="+453"/>
+        <source>Save image</source>
+        <translation type="unfinished">保存图像</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TimelineRow</name>
@@ -474,29 +484,6 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+161"/>
-        <source>Save image</source>
-        <translation type="unfinished">保存图像</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save video</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save audio</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Save file</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopRoomBar</name>
     <message>
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 4917e8930a3e72961339730ef9d7f8fa0a81580b..2c2ed02ad614fd90ce642e392aff7a45a51395d0 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -97,7 +97,7 @@ RowLayout {
 			MenuItem {
 				visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker
 				text: qsTr("Save as")
-				onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type)
+				onTriggered: timelineManager.timeline.saveMedia(model.id)
 			}
 		}
 	}
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index f4cf3f15d61b85b4640d37a2924eed918049b2a4..2c911c5ee1cb3359d2cfecfc5f614dfdfa83b7db 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -31,7 +31,7 @@ Rectangle {
 			}
 			MouseArea {
 				anchors.fill: parent
-				onClicked: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type)
+				onClicked: timelineManager.timeline.saveMedia(model.id)
 				cursorShape: Qt.PointingHandCursor
 			}
 		}
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index a1a06012feab10391c1286bd3c0dd5ea7ed7c240..1b6e572983825634d71f5a1c81a499f1f021dfa2 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -17,7 +17,7 @@ Item {
 		MouseArea {
 			enabled: model.type == MtxEvent.ImageMessage
 			anchors.fill: parent
-			onClicked: timelineManager.openImageOverlay(model.url, model.filename, model.mimetype, model.type)
+			onClicked: timelineManager.openImageOverlay(model.url, model.id)
 		}
 	}
 }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 3b987545c0935589934e03b8f6e5b9f854bf5fa6..d0d4d7cb7dd23413f356af2eb18e9fc8644bac30 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -97,7 +97,7 @@ Rectangle {
 					anchors.fill: parent
 					onClicked: {
 						switch (button.state) {
-							case "": timelineManager.cacheMedia(model.url, model.mimetype); break;
+							case "": timelineManager.timeline.cacheMedia(model.id); break;
 							case "stopped":
 							media.play(); console.log("play");
 							button.state = "playing"
@@ -118,7 +118,7 @@ Rectangle {
 				}
 
 				Connections {
-					target: timelineManager
+					target: timelineManager.timeline
 					onMediaCached: {
 						if (mxcUrl == model.url) {
 							media.source = "file://" + cacheUrl
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 091a9fa0b7812592016785e7a04ffaf471a153e6..d6f6940b60afc88299a2441a9e49998a6f3a39f3 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -54,6 +54,8 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
 constexpr int RETRY_TIMEOUT               = 5'000;
 constexpr size_t MAX_ONETIME_KEYS         = 50;
 
+Q_DECLARE_METATYPE(boost::optional<mtx::crypto::EncryptedFile>)
+
 ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
   : QWidget(parent)
   , isConnected_(true)
@@ -62,6 +64,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
 {
         setObjectName("chatPage");
 
+        qRegisterMetaType<boost::optional<mtx::crypto::EncryptedFile>>(
+          "boost::optional<mtx::crypto::EncryptedFile>");
+
         topLayout_ = new QHBoxLayout(this);
         topLayout_->setSpacing(0);
         topLayout_->setMargin(0);
@@ -299,9 +304,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
 
         connect(
           text_input_,
-          &TextInputWidget::uploadImage,
+          &TextInputWidget::uploadMedia,
           this,
-          [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+          [this](QSharedPointer<QIODevice> dev, QString mimeClass, const QString &fn) {
                   QMimeDatabase db;
                   QMimeType mime = db.mimeTypeForData(dev.data());
 
@@ -311,9 +316,18 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                           return;
                   }
 
-                  auto bin        = dev->peek(dev->size());
-                  auto payload    = std::string(bin.data(), bin.size());
-                  auto dimensions = QImageReader(dev.data()).size();
+                  auto bin     = dev->peek(dev->size());
+                  auto payload = std::string(bin.data(), bin.size());
+                  boost::optional<mtx::crypto::EncryptedFile> encryptedFile;
+                  if (cache::client()->isRoomEncrypted(current_room_.toStdString())) {
+                          mtx::crypto::BinaryBuf buf;
+                          std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
+                          payload                      = mtx::crypto::to_string(buf);
+                  }
+
+                  QSize dimensions;
+                  if (mimeClass == "image")
+                          dimensions = QImageReader(dev.data()).size();
 
                   http::client()->upload(
                     payload,
@@ -322,193 +336,61 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                     [this,
                      room_id  = current_room_,
                      filename = fn,
-                     mime     = mime.name(),
-                     size     = payload.size(),
+                     encryptedFile,
+                     mimeClass,
+                     mime = mime.name(),
+                     size = payload.size(),
                      dimensions](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: {} {} ({})",
+                                      tr("Failed to upload media. Please try again."));
+                                    nhlog::net()->warn("failed to upload media: {} {} ({})",
                                                        err->matrix_error.error,
                                                        to_string(err->matrix_error.errcode),
                                                        static_cast<int>(err->status_code));
                                     return;
                             }
 
-                            emit imageUploaded(room_id,
+                            emit mediaUploaded(room_id,
                                                filename,
+                                               encryptedFile,
                                                QString::fromStdString(res.content_uri),
+                                               mimeClass,
                                                mime,
                                                size,
                                                dimensions);
                     });
           });
 
-        connect(text_input_,
-                &TextInputWidget::uploadFile,
-                this,
-                [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::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> 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::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> 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::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(this, &ChatPage::uploadFailed, this, [this](const QString &msg) {
                 text_input_->hideUploadSpinner();
                 emit showNotification(msg);
         });
         connect(this,
-                &ChatPage::imageUploaded,
+                &ChatPage::mediaUploaded,
                 this,
                 [this](QString roomid,
                        QString filename,
+                       boost::optional<mtx::crypto::EncryptedFile> encryptedFile,
                        QString url,
+                       QString mimeClass,
                        QString mime,
                        qint64 dsize,
                        QSize dimensions) {
                         text_input_->hideUploadSpinner();
-                        view_manager_->queueImageMessage(
-                          roomid, filename, url, mime, dsize, dimensions);
-                });
-        connect(this,
-                &ChatPage::fileUploaded,
-                this,
-                [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 roomid, QString filename, QString url, QString mime, qint64 dsize) {
-                        text_input_->hideUploadSpinner();
-                        view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize);
-                });
-        connect(this,
-                &ChatPage::videoUploaded,
-                this,
-                [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
-                        text_input_->hideUploadSpinner();
-                        view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize);
+
+                        if (mimeClass == "image")
+                                view_manager_->queueImageMessage(
+                                  roomid, filename, encryptedFile, url, mime, dsize, dimensions);
+                        else if (mimeClass == "audio")
+                                view_manager_->queueAudioMessage(
+                                  roomid, filename, encryptedFile, url, mime, dsize);
+                        else if (mimeClass == "video")
+                                view_manager_->queueVideoMessage(
+                                  roomid, filename, encryptedFile, url, mime, dsize);
+                        else
+                                view_manager_->queueFileMessage(
+                                  roomid, filename, encryptedFile, url, mime, dsize);
                 });
 
         connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 1898f1a70f6397e2d527bc339d3d852700da5fb7..20e156af81aa9f018565bec39c658d8ac7bced47 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -18,7 +18,9 @@
 #pragma once
 
 #include <atomic>
+#include <boost/optional.hpp>
 #include <boost/variant.hpp>
+#include <mtx/common.hpp>
 #include <mtx/responses.hpp>
 
 #include <QFrame>
@@ -94,27 +96,14 @@ signals:
                                         const QPoint widgetPos);
 
         void uploadFailed(const QString &msg);
-        void imageUploaded(const QString &roomid,
+        void mediaUploaded(const QString &roomid,
                            const QString &filename,
+                           const boost::optional<mtx::crypto::EncryptedFile> &file,
                            const QString &url,
+                           const QString &mimeClass,
                            const QString &mime,
                            qint64 dsize,
                            const QSize &dimensions);
-        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();
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index 556b019b4178ea6d8d0543bfc9255483854e9066..edf6ceb5cec2b5f04883616534706e946f339c36 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -5,7 +5,7 @@
 void
 MxcImageResponse::run()
 {
-        if (m_requestedSize.isValid()) {
+        if (m_requestedSize.isValid() && !m_encryptionInfo) {
                 QString fileName = QString("%1_%2x%3_crop")
                                      .arg(m_id)
                                      .arg(m_requestedSize.width())
@@ -65,7 +65,12 @@ MxcImageResponse::run()
                                   return;
                           }
 
-                          auto data = QByteArray(res.data(), res.size());
+                          auto temp = res;
+                          if (m_encryptionInfo)
+                                  temp = mtx::crypto::to_string(
+                                    mtx::crypto::decrypt_file(temp, m_encryptionInfo.value()));
+
+                          auto data = QByteArray(temp.data(), temp.size());
                           m_image.loadFromData(data);
                           m_image.setText("original filename",
                                           QString::fromStdString(originalFilename));
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
index 19d8a74e23803b44101498502807699957343c60..2c197a13c552fe944be896c6f3c487210007c4e3 100644
--- a/src/MxcImageProvider.h
+++ b/src/MxcImageProvider.h
@@ -6,14 +6,21 @@
 #include <QImage>
 #include <QThreadPool>
 
+#include <mtx/common.hpp>
+
+#include <boost/optional.hpp>
+
 class MxcImageResponse
   : public QQuickImageResponse
   , public QRunnable
 {
 public:
-        MxcImageResponse(const QString &id, const QSize &requestedSize)
+        MxcImageResponse(const QString &id,
+                         const QSize &requestedSize,
+                         boost::optional<mtx::crypto::EncryptedFile> encryptionInfo)
           : m_id(id)
           , m_requestedSize(requestedSize)
+          , m_encryptionInfo(encryptionInfo)
         {
                 setAutoDelete(false);
         }
@@ -29,19 +36,34 @@ public:
         QString m_id, m_error;
         QSize m_requestedSize;
         QImage m_image;
+        boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo;
 };
 
-class MxcImageProvider : public QQuickAsyncImageProvider
+class MxcImageProvider
+  : public QObject
+  , public QQuickAsyncImageProvider
 {
-public:
+        Q_OBJECT
+public slots:
         QQuickImageResponse *requestImageResponse(const QString &id,
                                                   const QSize &requestedSize) override
         {
-                MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
+                boost::optional<mtx::crypto::EncryptedFile> info;
+                auto temp = infos.find("mxc://" + id);
+                if (temp != infos.end())
+                        info = *temp;
+
+                MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info);
                 pool.start(response);
                 return response;
         }
 
+        void addEncryptionInfo(mtx::crypto::EncryptedFile info)
+        {
+                infos.insert(QString::fromStdString(info.url), info);
+        }
+
 private:
         QThreadPool pool;
+        QHash<QString, mtx::crypto::EncryptedFile> infos;
 };
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index f723c01ac92774977deb9aef7896a9d89a051a13..66700dbc879f973604b95cea4c244d519f485f9f 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -458,21 +458,16 @@ FilteredTextEdit::textChanged()
 }
 
 void
-FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename)
+FilteredTextEdit::uploadData(const QByteArray data,
+                             const QString &mediaType,
+                             const QString &filename)
 {
         QSharedPointer<QBuffer> buffer{new QBuffer{this}};
         buffer->setData(data);
 
         emit startedUpload();
 
-        if (media == "image")
-                emit image(buffer, filename);
-        else if (media == "audio")
-                emit audio(buffer, filename);
-        else if (media == "video")
-                emit video(buffer, filename);
-        else
-                emit file(buffer, filename);
+        emit media(buffer, mediaType, filename);
 }
 
 void
@@ -580,10 +575,7 @@ TextInputWidget::TextInputWidget(QWidget *parent)
         connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage);
         connect(input_, &FilteredTextEdit::reply, this, &TextInputWidget::sendReplyMessage);
         connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command);
-        connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage);
-        connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio);
-        connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo);
-        connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile);
+        connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia);
         connect(emojiBtn_,
                 SIGNAL(emojiSelected(const QString &)),
                 this,
@@ -642,14 +634,8 @@ TextInputWidget::openFileSelection()
         const auto format = mime.name().split("/")[0];
 
         QSharedPointer<QFile> file{new QFile{fileName, this}};
-        if (format == "image")
-                emit uploadImage(file, fileName);
-        else if (format == "audio")
-                emit uploadAudio(file, fileName);
-        else if (format == "video")
-                emit uploadVideo(file, fileName);
-        else
-                emit uploadFile(file, fileName);
+
+        emit uploadMedia(file, format, fileName);
 
         showUploadSpinner();
 }
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 71f794d13e5be56343ffbcc88b1b709c7b89bbe5..d498be724ddfabd8be75622dcd38d053c45f78e3 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -63,10 +63,7 @@ signals:
         void message(QString);
         void reply(QString, const RelatedInfo &);
         void command(QString name, QString args);
-        void image(QSharedPointer<QIODevice> data, const QString &filename);
-        void audio(QSharedPointer<QIODevice> data, const QString &filename);
-        void video(QSharedPointer<QIODevice> data, const QString &filename);
-        void file(QSharedPointer<QIODevice> data, const QString &filename);
+        void media(QSharedPointer<QIODevice> data, QString mimeClass, const QString &filename);
 
         //! Trigger the suggestion popup.
         void showSuggestions(const QString &query);
@@ -179,10 +176,9 @@ signals:
         void sendEmoteMessage(QString msg);
         void heightChanged(int height);
 
-        void uploadImage(const QSharedPointer<QIODevice> data, const QString &filename);
-        void uploadFile(const QSharedPointer<QIODevice> data, const QString &filename);
-        void uploadAudio(const QSharedPointer<QIODevice> data, const QString &filename);
-        void uploadVideo(const QSharedPointer<QIODevice> data, const QString &filename);
+        void uploadMedia(const QSharedPointer<QIODevice> data,
+                         QString mimeClass,
+                         const QString &filename);
 
         void sendJoinRoomRequest(const QString &room);
 
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index b904dfd70ede49dd1e7cb8ad5343e42181d3697e..2c58e2f5803c742779d18e96abc22c3549748ccc 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -3,11 +3,15 @@
 #include <algorithm>
 #include <type_traits>
 
+#include <QFileDialog>
+#include <QMimeDatabase>
 #include <QRegularExpression>
+#include <QStandardPaths>
 
 #include "ChatPage.h"
 #include "Logging.h"
 #include "MainWindow.h"
+#include "MxcImageProvider.h"
 #include "Olm.h"
 #include "TimelineViewManager.h"
 #include "Utils.h"
@@ -88,17 +92,42 @@ eventFormattedBody(const mtx::events::RoomEvent<T> &e)
         }
 }
 
+template<class T>
+boost::optional<mtx::crypto::EncryptedFile>
+eventEncryptionInfo(const mtx::events::Event<T> &)
+{
+        return boost::none;
+}
+
+template<class T>
+auto
+eventEncryptionInfo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<
+  std::is_same<decltype(e.content.file), boost::optional<mtx::crypto::EncryptedFile>>::value,
+  boost::optional<mtx::crypto::EncryptedFile>>
+{
+        return e.content.file;
+}
+
 template<class T>
 QString
 eventUrl(const mtx::events::Event<T> &)
 {
         return "";
 }
+
+QString
+eventUrl(const mtx::events::StateEvent<mtx::events::state::Avatar> &e)
+{
+        return QString::fromStdString(e.content.url);
+}
+
 template<class T>
 auto
 eventUrl(const mtx::events::RoomEvent<T> &e)
   -> std::enable_if_t<std::is_same<decltype(e.content.url), std::string>::value, QString>
 {
+        if (e.content.file)
+                return QString::fromStdString(e.content.file->url);
         return QString::fromStdString(e.content.url);
 }
 
@@ -644,6 +673,19 @@ TimelineModel::internalAddEvents(
                         continue; // don't insert redaction into timeline
                 }
 
+                if (auto event =
+                      boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
+                        auto temp    = decryptEvent(*event).event;
+                        auto encInfo = boost::apply_visitor(
+                          [](const auto &ev) -> boost::optional<mtx::crypto::EncryptedFile> {
+                                  return eventEncryptionInfo(ev);
+                          },
+                          temp);
+
+                        if (encInfo)
+                                emit newEncryptedImage(encInfo.value());
+                }
+
                 this->events.insert(id, e);
                 ids.push_back(id);
         }
@@ -1342,3 +1384,158 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
         if (!isProcessingPending)
                 emit nextPendingMessage();
 }
+
+void
+TimelineModel::saveMedia(QString eventId) const
+{
+        mtx::events::collections::TimelineEvents event = events.value(eventId);
+
+        if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+                event = decryptEvent(*e).event;
+        }
+
+        QString mxcUrl =
+          boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event);
+        QString originalFilename =
+          boost::apply_visitor([](const auto &e) -> QString { return eventFilename(e); }, event);
+        QString mimeType =
+          boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event);
+
+        using EncF = boost::optional<mtx::crypto::EncryptedFile>;
+        EncF encryptionInfo =
+          boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event);
+
+        qml_mtx_events::EventType eventType = boost::apply_visitor(
+          [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event);
+
+        QString dialogTitle;
+        if (eventType == qml_mtx_events::EventType::ImageMessage) {
+                dialogTitle = tr("Save image");
+        } else if (eventType == qml_mtx_events::EventType::VideoMessage) {
+                dialogTitle = tr("Save video");
+        } else if (eventType == qml_mtx_events::EventType::AudioMessage) {
+                dialogTitle = tr("Save audio");
+        } else {
+                dialogTitle = tr("Save file");
+        }
+
+        QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
+
+        auto filename = QFileDialog::getSaveFileName(
+          manager_->getWidget(), dialogTitle, originalFilename, filterString);
+
+        if (filename.isEmpty())
+                return;
+
+        const auto url = mxcUrl.toStdString();
+
+        http::client()->download(
+          url,
+          [filename, url, encryptionInfo](const std::string &data,
+                                          const std::string &,
+                                          const std::string &,
+                                          mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
+                                             url,
+                                             err->matrix_error.error,
+                                             static_cast<int>(err->status_code));
+                          return;
+                  }
+
+                  try {
+                          auto temp = data;
+                          if (encryptionInfo)
+                                  temp = mtx::crypto::to_string(
+                                    mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+
+                          QFile file(filename);
+
+                          if (!file.open(QIODevice::WriteOnly))
+                                  return;
+
+                          file.write(QByteArray(temp.data(), (int)temp.size()));
+                          file.close();
+                  } catch (const std::exception &e) {
+                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
+                  }
+          });
+}
+
+void
+TimelineModel::cacheMedia(QString eventId)
+{
+        mtx::events::collections::TimelineEvents event = events.value(eventId);
+
+        if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+                event = decryptEvent(*e).event;
+        }
+
+        QString mxcUrl =
+          boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event);
+        QString mimeType =
+          boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event);
+
+        using EncF = boost::optional<mtx::crypto::EncryptedFile>;
+        EncF encryptionInfo =
+          boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event);
+
+        // If the message is a link to a non mxcUrl, don't download it
+        if (!mxcUrl.startsWith("mxc://")) {
+                emit mediaCached(mxcUrl, mxcUrl);
+                return;
+        }
+
+        QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
+
+        const auto url = mxcUrl.toStdString();
+        QFileInfo filename(QString("%1/media_cache/%2.%3")
+                             .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                             .arg(QString(mxcUrl).remove("mxc://"))
+                             .arg(suffix));
+        if (QDir::cleanPath(filename.path()) != filename.path()) {
+                nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
+                return;
+        }
+
+        QDir().mkpath(filename.path());
+
+        if (filename.isReadable()) {
+                emit mediaCached(mxcUrl, filename.filePath());
+                return;
+        }
+
+        http::client()->download(
+          url,
+          [this, mxcUrl, filename, url, encryptionInfo](const std::string &data,
+                                                        const std::string &,
+                                                        const std::string &,
+                                                        mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
+                                             url,
+                                             err->matrix_error.error,
+                                             static_cast<int>(err->status_code));
+                          return;
+                  }
+
+                  try {
+                          auto temp = data;
+                          if (encryptionInfo)
+                                  temp = mtx::crypto::to_string(
+                                    mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+
+                          QFile file(filename.filePath());
+
+                          if (!file.open(QIODevice::WriteOnly))
+                                  return;
+
+                          file.write(QByteArray(temp.data(), temp.size()));
+                          file.close();
+                  } catch (const std::exception &e) {
+                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
+                  }
+
+                  emit mediaCached(mxcUrl, filename.filePath());
+          });
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index e7842b99edcb19aa3108881710833b2d74c988bc..06c64acf5475aaedd5be7630bae52013b752f9fb 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -6,6 +6,7 @@
 #include <QHash>
 #include <QSet>
 
+#include <mtx/common.hpp>
 #include <mtx/responses.hpp>
 
 #include "Cache.h"
@@ -159,6 +160,8 @@ public:
         Q_INVOKABLE void redactEvent(QString id);
         Q_INVOKABLE int idToIndex(QString id) const;
         Q_INVOKABLE QString indexToId(int index) const;
+        Q_INVOKABLE void cacheMedia(QString eventId);
+        Q_INVOKABLE void saveMedia(QString eventId) const;
 
         void addEvents(const mtx::responses::Timeline &events);
         template<class T>
@@ -185,6 +188,8 @@ signals:
         void eventRedacted(QString id);
         void nextPendingMessage();
         void newMessageToSend(mtx::events::collections::TimelineEvents event);
+        void mediaCached(QString mxcUrl, QString cacheUrl);
+        void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
 
 private:
         DecryptionResult decryptEvent(
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 2a88c882a969581961f9aed469ce18d1de0dcdf6..6e18d111f1692760ff630ccfbc51eecd57a05521 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -1,11 +1,8 @@
 #include "TimelineViewManager.h"
 
-#include <QFileDialog>
 #include <QMetaType>
-#include <QMimeDatabase>
 #include <QPalette>
 #include <QQmlContext>
-#include <QStandardPaths>
 
 #include "ChatPage.h"
 #include "ColorImageProvider.h"
@@ -105,9 +102,14 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
 void
 TimelineViewManager::addRoom(const QString &room_id)
 {
-        if (!models.contains(room_id))
-                models.insert(room_id,
-                              QSharedPointer<TimelineModel>(new TimelineModel(this, room_id)));
+        if (!models.contains(room_id)) {
+                QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id));
+                connect(newRoom.data(),
+                        &TimelineModel::newEncryptedImage,
+                        imgProvider,
+                        &MxcImageProvider::addEncryptionInfo);
+                models.insert(room_id, std::move(newRoom));
+        }
 }
 
 void
@@ -124,146 +126,24 @@ TimelineViewManager::setHistoryView(const QString &room_id)
 }
 
 void
-TimelineViewManager::openImageOverlay(QString mxcUrl,
-                                      QString originalFilename,
-                                      QString mimeType,
-                                      qml_mtx_events::EventType eventType) const
+TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
 {
         QQuickImageResponse *imgResponse =
           imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
-        connect(imgResponse,
-                &QQuickImageResponse::finished,
-                this,
-                [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() {
-                        if (!imgResponse->errorString().isEmpty()) {
-                                nhlog::ui()->error("Error when retrieving image for overlay: {}",
-                                                   imgResponse->errorString().toStdString());
-                                return;
-                        }
-                        auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
+        connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() {
+                if (!imgResponse->errorString().isEmpty()) {
+                        nhlog::ui()->error("Error when retrieving image for overlay: {}",
+                                           imgResponse->errorString().toStdString());
+                        return;
+                }
+                auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
 
-                        auto imgDialog = new dialogs::ImageOverlay(pixmap);
-                        imgDialog->show();
-                        connect(imgDialog,
-                                &dialogs::ImageOverlay::saving,
-                                this,
-                                [this, mxcUrl, originalFilename, mimeType, eventType]() {
-                                        saveMedia(mxcUrl, originalFilename, mimeType, eventType);
-                                });
+                auto imgDialog = new dialogs::ImageOverlay(pixmap);
+                imgDialog->show();
+                connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() {
+                        timeline_->saveMedia(eventId);
                 });
-}
-
-void
-TimelineViewManager::saveMedia(QString mxcUrl,
-                               QString originalFilename,
-                               QString mimeType,
-                               qml_mtx_events::EventType eventType) const
-{
-        QString dialogTitle;
-        if (eventType == qml_mtx_events::EventType::ImageMessage) {
-                dialogTitle = tr("Save image");
-        } else if (eventType == qml_mtx_events::EventType::VideoMessage) {
-                dialogTitle = tr("Save video");
-        } else if (eventType == qml_mtx_events::EventType::AudioMessage) {
-                dialogTitle = tr("Save audio");
-        } else {
-                dialogTitle = tr("Save file");
-        }
-
-        QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
-
-        auto filename =
-          QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString);
-
-        if (filename.isEmpty())
-                return;
-
-        const auto url = mxcUrl.toStdString();
-
-        http::client()->download(
-          url,
-          [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;
-                  }
-
-                  try {
-                          QFile file(filename);
-
-                          if (!file.open(QIODevice::WriteOnly))
-                                  return;
-
-                          file.write(QByteArray(data.data(), (int)data.size()));
-                          file.close();
-                  } catch (const std::exception &e) {
-                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
-                  }
-          });
-}
-
-void
-TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType)
-{
-        // If the message is a link to a non mxcUrl, don't download it
-        if (!mxcUrl.startsWith("mxc://")) {
-                emit mediaCached(mxcUrl, mxcUrl);
-                return;
-        }
-
-        QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
-
-        const auto url = mxcUrl.toStdString();
-        QFileInfo filename(QString("%1/media_cache/%2.%3")
-                             .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
-                             .arg(QString(mxcUrl).remove("mxc://"))
-                             .arg(suffix));
-        if (QDir::cleanPath(filename.path()) != filename.path()) {
-                nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
-                return;
-        }
-
-        QDir().mkpath(filename.path());
-
-        if (filename.isReadable()) {
-                emit mediaCached(mxcUrl, filename.filePath());
-                return;
-        }
-
-        http::client()->download(
-          url,
-          [this, mxcUrl, 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;
-                  }
-
-                  try {
-                          QFile file(filename.filePath());
-
-                          if (!file.open(QIODevice::WriteOnly))
-                                  return;
-
-                          file.write(QByteArray(data.data(), data.size()));
-                          file.close();
-                  } catch (const std::exception &e) {
-                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
-                  }
-
-                  emit mediaCached(mxcUrl, filename.filePath());
-          });
+        });
 }
 
 void
@@ -342,6 +222,7 @@ TimelineViewManager::queueEmoteMessage(const QString &msg)
 void
 TimelineViewManager::queueImageMessage(const QString &roomid,
                                        const QString &filename,
+                                       const boost::optional<mtx::crypto::EncryptedFile> &file,
                                        const QString &url,
                                        const QString &mime,
                                        uint64_t dsize,
@@ -354,27 +235,32 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
         image.url           = url.toStdString();
         image.info.h        = dimensions.height();
         image.info.w        = dimensions.width();
+        image.file          = file;
         models.value(roomid)->sendMessage(image);
 }
 
 void
-TimelineViewManager::queueFileMessage(const QString &roomid,
-                                      const QString &filename,
-                                      const QString &url,
-                                      const QString &mime,
-                                      uint64_t dsize)
+TimelineViewManager::queueFileMessage(
+  const QString &roomid,
+  const QString &filename,
+  const boost::optional<mtx::crypto::EncryptedFile> &encryptedFile,
+  const QString &url,
+  const QString &mime,
+  uint64_t dsize)
 {
         mtx::events::msg::File file;
         file.info.mimetype = mime.toStdString();
         file.info.size     = dsize;
         file.body          = filename.toStdString();
         file.url           = url.toStdString();
+        file.file          = encryptedFile;
         models.value(roomid)->sendMessage(file);
 }
 
 void
 TimelineViewManager::queueAudioMessage(const QString &roomid,
                                        const QString &filename,
+                                       const boost::optional<mtx::crypto::EncryptedFile> &file,
                                        const QString &url,
                                        const QString &mime,
                                        uint64_t dsize)
@@ -384,12 +270,14 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
         audio.info.size     = dsize;
         audio.body          = filename.toStdString();
         audio.url           = url.toStdString();
+        audio.file          = file;
         models.value(roomid)->sendMessage(audio);
 }
 
 void
 TimelineViewManager::queueVideoMessage(const QString &roomid,
                                        const QString &filename,
+                                       const boost::optional<mtx::crypto::EncryptedFile> &file,
                                        const QString &url,
                                        const QString &mime,
                                        uint64_t dsize)
@@ -399,5 +287,6 @@ TimelineViewManager::queueVideoMessage(const QString &roomid,
         video.info.size     = dsize;
         video.body          = filename.toStdString();
         video.url           = url.toStdString();
+        video.file          = file;
         models.value(roomid)->sendMessage(video);
 }
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 0bc58e683d57e545c0ebeb128ff98a989f715bc3..9e8de616c08974abaf51a320136047ac4e12dd2d 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -5,6 +5,7 @@
 #include <QSharedPointer>
 #include <QWidget>
 
+#include <mtx/common.hpp>
 #include <mtx/responses.hpp>
 
 #include "Cache.h"
@@ -35,38 +36,13 @@ public:
 
         Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
         Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
-        void openImageOverlay(QString mxcUrl,
-                              QString originalFilename,
-                              QString mimeType,
-                              qml_mtx_events::EventType eventType) const;
-        void saveMedia(QString mxcUrl,
-                       QString originalFilename,
-                       QString mimeType,
-                       qml_mtx_events::EventType eventType) const;
-        Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType);
-        // Qml can only pass enum as int
-        Q_INVOKABLE void openImageOverlay(QString mxcUrl,
-                                          QString originalFilename,
-                                          QString mimeType,
-                                          int eventType) const
-        {
-                openImageOverlay(
-                  mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
-        }
-        Q_INVOKABLE void saveMedia(QString mxcUrl,
-                                   QString originalFilename,
-                                   QString mimeType,
-                                   int eventType) const
-        {
-                saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
-        }
+        Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
 
 signals:
         void clearRoomMessageCount(QString roomid);
         void updateRoomsLastMessage(QString roomid, const DescInfo &info);
         void activeTimelineChanged(TimelineModel *timeline);
         void initialSyncChanged(bool isInitialSync);
-        void mediaCached(QString mxcUrl, QString cacheUrl);
 
 public slots:
         void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
@@ -80,22 +56,26 @@ public slots:
         void queueEmoteMessage(const QString &msg);
         void queueImageMessage(const QString &roomid,
                                const QString &filename,
+                               const boost::optional<mtx::crypto::EncryptedFile> &file,
                                const QString &url,
                                const QString &mime,
                                uint64_t dsize,
                                const QSize &dimensions);
         void queueFileMessage(const QString &roomid,
                               const QString &filename,
+                              const boost::optional<mtx::crypto::EncryptedFile> &file,
                               const QString &url,
                               const QString &mime,
                               uint64_t dsize);
         void queueAudioMessage(const QString &roomid,
                                const QString &filename,
+                               const boost::optional<mtx::crypto::EncryptedFile> &file,
                                const QString &url,
                                const QString &mime,
                                uint64_t dsize);
         void queueVideoMessage(const QString &roomid,
                                const QString &filename,
+                               const boost::optional<mtx::crypto::EncryptedFile> &file,
                                const QString &url,
                                const QString &mime,
                                uint64_t dsize);