diff --git a/.ci/format.sh b/.ci/format.sh
index d3b629c3bfce13c7a789ea1b687514fbf137e52e..e1e6c1e41e78c6473f4a50136c1501439c9d92bc 100755
--- a/.ci/format.sh
+++ b/.ci/format.sh
@@ -11,5 +11,7 @@ FILES=$(find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \))
 
 for f in $FILES
 do
-    clang-format -i "$f" && git diff --exit-code
+    clang-format -i "$f"
 done;
+
+git diff --exit-code
diff --git a/.ci/install.sh b/.ci/install.sh
index c1f4235736b178a5ef4977c6d15f2901bf192d95..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
@@ -54,5 +54,7 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
         qt${QT_PKG}tools \
         qt${QT_PKG}svg \
         qt${QT_PKG}multimedia \
+        qt${QT_PKG}quickcontrols2 \
+        qt${QT_PKG}graphicaleffects \
         liblmdb-dev
 fi
diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh
index 2caf5e0f8a6ad7127d2be0a08b79a5471899ed50..524d72d50db1f68d7c089b5d8b4db76ab4eda00f 100755
--- a/.ci/linux/deploy.sh
+++ b/.ci/linux/deploy.sh
@@ -44,8 +44,7 @@ do
     linuxdeployqt=$res
 done
 
-./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs
-./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -appimage
+./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs -qmldir=./resources/qml -appimage
 
 chmod +x nheko-*x86_64.AppImage
 
diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh
index ee4acaede401126ecbfd87a9a277e4fbb1b87086..1dc9472d7bfac5f975388d6611f4a1f525c90ad0 100755
--- a/.ci/macos/deploy.sh
+++ b/.ci/macos/deploy.sh
@@ -16,7 +16,7 @@ PATH=/usr/local/opt/qt/bin/:${PATH}
   mkdir -p nheko.app/Contents/Frameworks
   find "${ICU_LIB}" -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true
 
-  sudo macdeployqt nheko.app -dmg -always-overwrite
+  sudo macdeployqt nheko.app -dmg -always-overwrite -qmldir=../resources/qml/
 
   user=$(id -nu)
   sudo chown "${user}" nheko.dmg
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/.gitignore b/.gitignore
index 43c9b7b4aa81f8ef44e27a0d860aa22f20aa9113..2d772e581911128df766a031e84aecf286c6b954 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,11 @@
-build
+/build*
 tags
 cscope*
 .clang_complete
 *wintoastlib*
+/.ccls-cache
+/.exrc
+.gdb_history
 
 # GTAGS
 GTAGS
@@ -49,6 +52,10 @@ ui_*.h
 *.qmlproject.user
 *.qmlproject.user.*
 
+# Vim
+*.swp
+*.swo
+
 #####=== CMake ===#####
 
 CMakeCache.txt
diff --git a/.travis.yml b/.travis.yml
index 6a699043b0fff4f2674f9fe64bdf500d6cf86675..4ab6408a83c8290ad7ef205349ce2d2088467da3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,7 +15,8 @@ matrix:
     include:
         - os: osx
           compiler: clang
-          osx_image: xcode9
+          # Use the default osx image, because that one is actually tested to work with homebrew and probably the oldest supported version
+          #          osx_image: xcode9
           env:
               - DEPLOYMENT=1
               - USE_BUNDLED_BOOST=0
@@ -42,8 +43,8 @@ matrix:
           env:
               - CXX_COMPILER=g++-8
               - C_COMPILER=gcc-8
-              - QT_VERSION=571
-              - QT_PKG=57
+              - QT_VERSION=592
+              - QT_PKG=59
               - USE_BUNDLED_BOOST=1
               - USE_BUNDLED_CMARK=1
               - USE_BUNDLED_JSON=1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4d5aff7aa49e9deac2c447c1912569f528f37c55..67a1dfb09c1074ddcdc27bbfb286669e809a9b79 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -69,7 +69,8 @@ include(LMDB)
 #
 # Discover Qt dependencies.
 #
-find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED)
+find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED)
+find_package(Qt5QuickCompiler)
 find_package(Qt5DBus)
 
 if (APPLE)
@@ -192,12 +193,8 @@ set(SRC_FILES
 
     # Timeline
     src/timeline/TimelineViewManager.cpp
-    src/timeline/TimelineItem.cpp
-    src/timeline/TimelineView.cpp
-    src/timeline/widgets/AudioItem.cpp
-    src/timeline/widgets/FileItem.cpp
-    src/timeline/widgets/ImageItem.cpp
-    src/timeline/widgets/VideoItem.cpp
+    src/timeline/TimelineModel.cpp
+    src/timeline/DelegateChooser.cpp
 
     # UI components
     src/ui/Avatar.cpp
@@ -229,6 +226,8 @@ set(SRC_FILES
     src/Logging.cpp
     src/MainWindow.cpp
     src/MatrixClient.cpp
+    src/MxcImageProvider.cpp
+    src/ColorImageProvider.cpp
     src/QuickSwitcher.cpp
     src/Olm.cpp
     src/RegisterPage.cpp
@@ -260,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
@@ -333,13 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS
     src/emoji/PickButton.h
 
     # Timeline
-    src/timeline/TimelineItem.h
-    src/timeline/TimelineView.h
     src/timeline/TimelineViewManager.h
-    src/timeline/widgets/AudioItem.h
-    src/timeline/widgets/FileItem.h
-    src/timeline/widgets/ImageItem.h
-    src/timeline/widgets/VideoItem.h
+    src/timeline/TimelineModel.h
+    src/timeline/DelegateChooser.h
 
     # UI components
     src/ui/Avatar.h
@@ -370,7 +365,7 @@ qt5_wrap_cpp(MOC_HEADERS
     src/CommunitiesList.h
     src/LoginPage.h
     src/MainWindow.h
-    src/MatrixClient.h
+    src/MxcImageProvider.h
     src/InviteeItem.h
     src/QuickSwitcher.h
     src/RegisterPage.h
@@ -405,6 +400,9 @@ set(COMMON_LIBS
     Qt5::Svg
     Qt5::Concurrent
     Qt5::Multimedia
+    Qt5::Qml
+    Qt5::QuickControls2
+    Qt5::QuickWidgets
     nlohmann_json::nlohmann_json)
 
 if(APPVEYOR_BUILD)
@@ -448,6 +446,7 @@ if(APPLE)
     target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras)
 elseif(WIN32)
     add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS})
+    target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN)
     target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain)
 else()
     add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
diff --git a/Dockerfile b/Dockerfile
index 2e01b40bfce8df0b95238e2fbff74902bc46b8aa..dddd1c6f71c996407b286d27b19b69753f5ad640 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,7 +7,7 @@ RUN \
     add-apt-repository -y ppa:ubuntu-toolchain-r/test && \
     apt-get update -qq && \
     apt-get install -y \
-        qt510base qt510tools qt510svg qt510multimedia \
+        qt510base qt510tools qt510svg qt510multimedia qt510quickcontrols2 qt510graphicaleffects \
         gcc-5 g++-5
 
 RUN \
@@ -44,4 +44,4 @@ ENV PATH=/opt/qt510/bin:$PATH
 
 RUN mkdir /build
 
-WORKDIR /build
\ No newline at end of file
+WORKDIR /build
diff --git a/Makefile b/Makefile
index 2f688d3bbfff562b301cac9ca1c1a3d4c6292bc6..7f603dcb8593c56f16499cd1ab07e6adc07e4316 100644
--- a/Makefile
+++ b/Makefile
@@ -68,7 +68,7 @@ update-translations:
 		-locations relative \
 		-Iinclude/dialogs \
 		-Iinclude \
-		src/ -ts resources/langs/nheko_*.ts -no-obsolete
+		src/ resources/qml/ -ts resources/langs/nheko_*.ts -no-obsolete
 
 clean:
 	rm -rf build
diff --git a/README.md b/README.md
index 1a9609abf58f11eca1c1299768a461a464a48b2c..1179463da576c2e461c0514e89e6956554921992 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,11 @@ nheko
 The motivation behind the project is to provide a native desktop app for [Matrix] that
 feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IRC client.
 
+### Translations ###
+[![Translation status](http://weblate.nheko.im/widgets/nheko/-/nheko-master/svg-badge.svg)](http://weblate.nheko.im/engage/nheko/?utm_source=widget)
+
+Help us with translations so as many people as possible will be able to use nheko!
+
 ### Note regarding End-to-End encryption
 
 Currently the implementation is at best a **proof of concept** and it should only be used for
@@ -84,13 +89,14 @@ sudo port install nheko
 
 ### Build Requirements
 
-- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with
-  Freetype, which is essential to properly support emoji.
-- CMake 3.1 or greater.
+- Qt5 (5.9 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.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)
@@ -126,7 +132,7 @@ sudo pacman -S qt5-base \
 ##### Gentoo Linux
 
 ```bash
-sudo emerge -a ">=dev-qt/qtgui-5.7.1" media-libs/fontconfig
+sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig
 ```
 
 ##### Ubuntu (e.g 14.04)
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/cmake/Translations.cmake b/cmake/Translations.cmake
index 8ca918830c8ddcc661a5d9ea7455c4608ae3dce8..16120219eed4b6998eb0db0924de3b9cdd2a1e87 100644
--- a/cmake/Translations.cmake
+++ b/cmake/Translations.cmake
@@ -21,4 +21,8 @@ if(NOT EXISTS ${_qrc})
 endif()
 
 qt5_add_resources(LANG_QRC ${_qrc})
-qt5_add_resources(QRC resources/res.qrc)
+if(Qt5QuickCompiler_FOUND)
+	qtquick_compiler_add_resources(QRC resources/res.qrc)
+else()
+	qt5_add_resources(QRC resources/res.qrc)
+endif()
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/deps/cmake/Boost.cmake b/deps/cmake/Boost.cmake
index 47eb723bac1b29131f5f6849ce2a144b8826af65..5d11fd938403d4293ffd99f2c1478d0809e0ffcd 100644
--- a/deps/cmake/Boost.cmake
+++ b/deps/cmake/Boost.cmake
@@ -3,6 +3,10 @@ if(WIN32)
   return()
 endif()
 
+include(BoostToolsetId)
+set(BOOST_TOOLSET "gcc")
+Boost_Get_ToolsetId(BOOST_TOOLSET)
+
 ExternalProject_Add(
   Boost
 
@@ -16,6 +20,7 @@ ExternalProject_Add(
   CONFIGURE_COMMAND ${DEPS_BUILD_DIR}/boost/bootstrap.sh
     --with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex
     --prefix=${DEPS_INSTALL_DIR}
+    --with-toolset=${BOOST_TOOLSET}
   BUILD_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 cxxstd=14 variant=release link=shared runtime-link=shared threading=multi --layout=system
   INSTALL_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 install
 )
diff --git a/deps/cmake/BoostToolsetId.cmake b/deps/cmake/BoostToolsetId.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..f6c231a51bdaf5d8002545ba0f0ef1e38ec8476c
--- /dev/null
+++ b/deps/cmake/BoostToolsetId.cmake
@@ -0,0 +1,35 @@
+# - Translate CMake compilers to the Boost.Build toolset equivalents
+# To build Boost reliably when a non-system compiler may be used, we
+# need to both specify the toolset when running bootstrap.sh *and* in
+# the user-config.jam file.
+#
+# This module provides the following functions to help translate between
+# the systems:
+#
+#  function Boost_Get_ToolsetId(<var>)
+#           Set var equal to Boost's name for the CXX toolchain picked
+#           up by CMake. Only supports GNU and Clang families at present.
+#           Intel support is provisional
+#
+# downloaded from https://github.com/drbenmorgan/BoostBuilder/blob/master/BoostToolsetId.cmake
+
+function(Boost_Get_ToolsetId _var)
+  set(BOOST_TOOLSET)
+
+  if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
+    if(APPLE)
+      set(BOOST_TOOLSET "darwin")
+    else()
+      set(BOOST_TOOLSET "gcc")
+    endif()
+  elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
+    set(BOOST_TOOLSET "clang")
+  elseif(CMAKE_CXX_COMPILER_ID MATCHES "Intel")
+    set(BOOST_TOOLSET "intel")
+  elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
+    set(BOOST_TOOLSET "msvc")
+  endif()
+
+  set(${_var} ${BOOST_TOOLSET} PARENT_SCOPE)
+endfunction()
+
diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts
index e92bf966091f2626b8c80e012a277748f911a8be..59c6dffd85c5cc196f6c2ec2214279f0f3fd521a 100644
--- a/resources/langs/nheko_de.ts
+++ b/resources/langs/nheko_de.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="de">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>In Datei speichern</translation>
-    </message>
-</context>
 <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>
-    </message>
-    <message>
-        <location line="+45"/>
-        <source>Failed to upload file. Please try again.</source>
-        <translation>Hochladen der Datei 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="+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="+380"/>
+        <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>
@@ -42,18 +19,18 @@
         <translation>Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.</translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Fehler beim Setup der Verschlüsselungsschlüssel. Servermeldung: %1 %2. Bitte versuche es später erneut.</translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation>Bitte melde dich erneut an: %1</translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation>Raum konnte nicht erstellt werden: %1</translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Datei speichern</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Bild speichern</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation>Verschlüsselt</translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Teilnehmerliste</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation>unimplementiertes event: </translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation>keine Version gespeichert</translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Raum verlassen</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Akzeptieren</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
-        <translation>Verschlüsselt</translation>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
+        <translation>Fehlgeschlagen</translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
-        <translation>Erhalten</translation>
+        <location line="+1"/>
+        <source>Sent</source>
+        <translation>Gesendet</translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
-        <translation>Gelesen</translation>
+        <location line="+1"/>
+        <source>Received</source>
+        <translation>Empfangen</translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
-        <translation>Gesendet</translation>
+        <location line="+1"/>
+        <source>Read</source>
+        <translation>Gelesen</translation>
     </message>
 </context>
 <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>
@@ -391,32 +381,9 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
-    <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
-        <source>Message redaction failed: %1</source>
-        <translation>Nachricht zurückziehen fehlgeschlagen: %1</translation>
-    </message>
-    <message>
-        <location line="+39"/>
-        <source>Reply</source>
-        <translation>Antworten</translation>
-    </message>
-    <message>
-        <location line="+11"/>
-        <source>Options</source>
-        <translation>Optionen</translation>
-    </message>
-</context>
-<context>
-    <name>TimelineView</name>
-    <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
-        <translation>Verschlüsselung aktiv</translation>
-    </message>
+    <name>TimelineModel</name>
     <message>
-        <location line="+65"/>
+        <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>
@@ -440,16 +407,87 @@
         <translation>-- Entschlüsselungsfehler (%1) --</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+25"/>
         <source>-- Encrypted Event (Unknown event type) --</source>
         <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
         <translation>-- verschlüsselter Event (Unbekannter Eventtyp) --</translation>
     </message>
+    <message>
+        <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>
+    <message>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
+        <translation>Antworten</translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Options</source>
+        <translation>Optionen</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation>Lesebestätigungen</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
+        <translation>Als gelesen markieren</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>View raw message</source>
+        <translation>Zeige rohen Nachrichteninhalt</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Redact message</source>
+        <translation>Nachricht löschen</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation>Speichern als...</translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
+        <translation>Kein Raum geöffnet</translation>
+    </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation>Raumoptionen</translation>
     </message>
@@ -515,7 +553,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Ins Benachrichtigungsfeld minimieren</translation>
     </message>
@@ -529,6 +567,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>Gruppen-Seitenleiste</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation>Runde Profilbilder</translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -605,7 +648,7 @@
         <translation>ALLGEMEINES</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation>Öffne Sessions Datei</translation>
     </message>
@@ -825,7 +868,7 @@ Medien-Größe: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>Lesebestätigungen</translation>
     </message>
@@ -951,7 +994,7 @@ Medien-Größe: %2
         <translation>Aktivierung der Verschlüsselung fehlgeschlagen: %1</translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation>Wähle einen Avatar</translation>
     </message>
@@ -977,19 +1020,6 @@ Medien-Größe: %2
         <translation>Hochladen der Bilddatei fehlgeschlagen: %s</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation>Dieser Raum</translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation>Alle Räume</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1013,7 +1043,7 @@ Medien-Größe: %2
         <translation>Gespräch beginnen</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation>Geräte</translation>
     </message>
@@ -1064,69 +1094,103 @@ Medien-Größe: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
-        <translation>%1 einen Audioclip</translation>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
+        <translation>Du hast eine Audiodatei gesendet.</translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
-        <translation>%1 ein Bild</translation>
+        <source>%1 sent an audio clip</source>
+        <translation>%1 hat eine Audiodatei gesendet.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation>Du hast ein Bild gesendet.</translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
-        <translation>%1 eine Datei</translation>
+        <source>%1 sent an image</source>
+        <translation>%1 hat ein Bild gesendet.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
+        <translation>Du hast eine Datei gesendet.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a file</source>
+        <translation>%1 hat eine Datei gesendet.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation>Du hast ein Video gesendet.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a video</source>
+        <translation>%1 hat ein Video gesendet.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
+        <translation>Du hast einen Sticker gesendet.</translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
-        <translation>%1 einen Videoclip</translation>
+        <source>%1 sent a sticker</source>
+        <translation>%1 hat einen Sticker gesendet.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
+        <translation>Du hast eine Benachrichtigung gesendet.</translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
-        <translation>%1 einen Sticker</translation>
+        <source>%1 sent a notification</source>
+        <translation>%1 hat eine Benachrichtigung gesendet.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
+        <translation>Du: %1</translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
-        <translation>1% eine Benachrichtigung</translation>
+        <source>%1: %2</source>
+        <translation>%1: %2</translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
-        <translation>1% eine verschüsselte Nachricht</translation>
+        <source>You sent an encrypted message</source>
+        <translation>Du hast eine verschlüsselte Nachricht gesendet.</translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
-        <translation type="unfinished"></translation>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
+        <translation>%1 hat eine verschlüsselte Nachricht gesendet.</translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
-        <translation type="unfinished"></translation>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
+        <translation>Dieser Raum</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>All Rooms</source>
+        <translation>Alle Räume</translation>
     </message>
 </context>
 <context>
     <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
-        <translation>Du</translation>
-    </message>
-    <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1146,7 +1210,7 @@ Medien-Größe: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation>Unbekannter Nachrichtentyp</translation>
     </message>
diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts
index 700c3d575fb6ecac4e3bdbf3f396dc6efa5d522f..fe65785bdaf2a0e1b33be757ca7b01977fb366cd 100644
--- a/resources/langs/nheko_el.ts
+++ b/resources/langs/nheko_el.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="el">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>Αποθήκευση</translation>
-    </message>
-</context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. 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>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. 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="+380"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -42,18 +19,18 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Αποθήκευση</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Αποθήκευση Εικόνας</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Μέλη</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Βγές</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Αποδοχή</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
+        <location line="+1"/>
+        <source>Sent</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
+        <location line="+1"/>
+        <source>Received</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
+        <location line="+1"/>
+        <source>Read</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -391,65 +381,113 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
+    <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
+        <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>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>-- Decryption Error (failed to communicate with DB) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+19"/>
+        <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>-- Decryption Error (%1) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+25"/>
+        <source>-- Encrypted Event (Unknown event type) --</source>
+        <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+39"/>
-        <source>Reply</source>
+        <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="+11"/>
-        <source>Options</source>
+        <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>TimelineView</name>
+    <name>TimelineRow</name>
     <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+65"/>
-        <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>
+        <location line="+14"/>
+        <source>Options</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
-        <source>-- Decryption Error (failed to communicate with DB) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
+        <location line="+12"/>
+        <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
-        <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
+        <location line="+4"/>
+        <source>Mark as read</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
-        <source>-- Decryption Error (%1) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
+        <location line="+3"/>
+        <source>View raw message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
-        <source>-- Encrypted Event (Unknown event type) --</source>
-        <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
+        <location line="+4"/>
+        <source>Redact message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -515,7 +553,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Ελαχιστοποίηση</translation>
     </message>
@@ -529,6 +567,11 @@
         <source>Group&apos;s sidebar</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -605,7 +648,7 @@
         <translation>ΓΕΝΙΚΑ</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
@@ -823,7 +866,7 @@ Media size: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
@@ -949,7 +992,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -975,19 +1018,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1011,7 +1041,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1062,69 +1092,103 @@ Media size: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
+        <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
+        <source>You sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
+        <location line="+1"/>
+        <source>All Rooms</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>utils</name>
     <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1144,7 +1208,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts
index edb953137793d052c5bb195af44da0747220da10..49ea7439b3de103141e7e7e58b59e513e66a9cd9 100644
--- a/resources/langs/nheko_en.ts
+++ b/resources/langs/nheko_en.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="en">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>Save File</translation>
-    </message>
-</context>
 <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="+380"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Failed to restore OLM account. Please login again.</translation>
     </message>
@@ -42,18 +19,18 @@
         <translation>Failed to restore save data. Please login again.</translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation>Please try to login again: %1</translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation>Room creation failed: %1</translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Save File</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Save image</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Room members</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation>no version stored</translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Leave room</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Accept</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
-        <translation>Encrypted</translation>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
-        <translation>Delivered</translation>
+        <location line="+1"/>
+        <source>Sent</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
-        <translation>Seen</translation>
+        <location line="+1"/>
+        <source>Received</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
-        <translation>Sent</translation>
+        <location line="+1"/>
+        <source>Read</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -391,65 +381,113 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
-    <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
-        <source>Message redaction failed: %1</source>
-        <translation>Message redaction failed: %1</translation>
-    </message>
-    <message>
-        <location line="+39"/>
-        <source>Reply</source>
-        <translation>Reply</translation>
-    </message>
-    <message>
-        <location line="+11"/>
-        <source>Options</source>
-        <translation>Options</translation>
-    </message>
-</context>
-<context>
-    <name>TimelineView</name>
+    <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
-        <translation>Encryption is enabled</translation>
-    </message>
-    <message>
-        <location line="+65"/>
+        <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>-- Encrypted Event (No keys found for decryption) --</translation>
+        <translation type="unfinished">-- Encrypted Event (No keys found for decryption) --</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>-- Decryption Error (failed to communicate with DB) --</source>
         <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
-        <translation>-- Decryption Error (failed to communicate with DB) --</translation>
+        <translation type="unfinished">-- Decryption Error (failed to communicate with DB) --</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
         <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
-        <translation>-- Decryption Error (failed to retrieve megolm keys from db) --</translation>
+        <translation type="unfinished">-- Decryption Error (failed to retrieve megolm keys from db) --</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>-- Decryption Error (%1) --</source>
         <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
-        <translation>-- Decryption Error (%1) --</translation>
+        <translation type="unfinished">-- Decryption Error (%1) --</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+25"/>
         <source>-- Encrypted Event (Unknown event type) --</source>
         <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
-        <translation>-- Encrypted Event (Unknown event type) --</translation>
+        <translation type="unfinished">-- Encrypted Event (Unknown event type) --</translation>
+    </message>
+    <message>
+        <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>
+    <message>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Options</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Read receipts</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>View raw message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Redact message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation>Room options</translation>
     </message>
@@ -515,7 +553,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Minimize to tray</translation>
     </message>
@@ -529,6 +567,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>Group&apos;s sidebar</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -605,7 +648,7 @@
         <translation>GENERAL</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation>Open Sessions File</translation>
     </message>
@@ -825,7 +868,7 @@ Media size: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>Read receipts</translation>
     </message>
@@ -953,7 +996,7 @@ Media size: %2
         <translation>Failed to enable encryption: %1</translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation>Select an avatar</translation>
     </message>
@@ -979,19 +1022,6 @@ Media size: %2
         <translation>Failed to upload image: %s</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation>This Room</translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation>All Rooms</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1015,7 +1045,7 @@ Media size: %2
         <translation>Start a conversation</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation>Devices</translation>
     </message>
@@ -1066,69 +1096,103 @@ Media size: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
-        <translation>%1 an audio clip</translation>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
-        <translation>%1 an image</translation>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
-        <translation>%1 a file</translation>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
-        <translation>%1 a video clip</translation>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
-        <translation>%1 a sticker</translation>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
-        <translation>%1 a notification</translation>
+        <source>%1: %2</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
-        <translation>%1 an encrypted message</translation>
+        <source>You sent an encrypted message</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
-        <translation>sent</translation>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
-        <translation>sent</translation>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
+        <translation type="unfinished">This Room</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>All Rooms</source>
+        <translation type="unfinished">All Rooms</translation>
     </message>
 </context>
 <context>
     <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
-        <translation>You</translation>
-    </message>
-    <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation>sent a file.</translation>
     </message>
@@ -1148,7 +1212,7 @@ Media size: %2
         <translation>sent a video.</translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation>Unknown Message Type</translation>
     </message>
diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts
index 89eb33b7874c70e5f7eca92dc184da5513862787..4bb20e3098f0a40afa07d6d26ffc5e629812549f 100644
--- a/resources/langs/nheko_fi.ts
+++ b/resources/langs/nheko_fi.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="fi">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>Tallenna tiedosto</translation>
-    </message>
-</context>
 <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="+380"/>
+        <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>
@@ -42,18 +19,18 @@
         <translation>Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Salausavainten lähetys epäonnistui. Palvelimen vastaus: %1 %2. Ole hyvä ja yritä uudelleen myöhemmin.</translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation>Ole hyvä ja yritä kirjautua sisään uudelleen: %1</translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation>Huoneen luominen epäonnistui: %1</translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Tallenna tiedosto</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Tallenna kuva</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Huoneen jäsenet</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation>ei tallennettua versiota</translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Poistu huoneesta</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Hyväksy</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
-        <translation>Salattu</translation>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
-        <translation>Toimitettu</translation>
+        <location line="+1"/>
+        <source>Sent</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
-        <translation>Luettu</translation>
+        <location line="+1"/>
+        <source>Received</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
-        <translation>Lähetetty</translation>
+        <location line="+1"/>
+        <source>Read</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -391,65 +381,113 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
-    <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
-        <source>Message redaction failed: %1</source>
-        <translation>Viestin poisto epäonnistui: %1</translation>
-    </message>
-    <message>
-        <location line="+39"/>
-        <source>Reply</source>
-        <translation>Vastaa</translation>
-    </message>
-    <message>
-        <location line="+11"/>
-        <source>Options</source>
-        <translation>Asetukset</translation>
-    </message>
-</context>
-<context>
-    <name>TimelineView</name>
+    <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
-        <translation>Salaus on käytössä</translation>
-    </message>
-    <message>
-        <location line="+65"/>
+        <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>-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation>
+        <translation type="unfinished">-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>-- Decryption Error (failed to communicate with DB) --</source>
         <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
-        <translation>-- Virhe purkaessa salausta (tietokannan kanssa kommunikointi epäonnistui) --</translation>
+        <translation type="unfinished">-- Virhe purkaessa salausta (tietokannan kanssa kommunikointi epäonnistui) --</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
         <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
-        <translation>-- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) --</translation>
+        <translation type="unfinished">-- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) --</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>-- Decryption Error (%1) --</source>
         <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
-        <translation>-- Virhe purkaessa salausta (%1) --</translation>
+        <translation type="unfinished">-- Virhe purkaessa salausta (%1) --</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+25"/>
         <source>-- Encrypted Event (Unknown event type) --</source>
         <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
-        <translation>-- Salattu viesti (tuntematon viestityyppi) --</translation>
+        <translation type="unfinished">-- Salattu viesti (tuntematon viestityyppi) --</translation>
+    </message>
+    <message>
+        <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>
+    <message>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Options</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Lukukuittaukset</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>View raw message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Redact message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation>Huonevaihtoehdot</translation>
     </message>
@@ -515,7 +553,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Pienennä ilmoitusalueelle</translation>
     </message>
@@ -529,6 +567,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>Ryhmäsivupalkki</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -605,7 +648,7 @@
         <translation>YLEISET ASETUKSET</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation>Avaa Istuntoavaintiedosto</translation>
     </message>
@@ -825,7 +868,7 @@ Median koko: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>Lukukuittaukset</translation>
     </message>
@@ -953,7 +996,7 @@ Median koko: %2
         <translation>Salauksen aktivointi epäonnistui: %1</translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation>Valitse profiilikuva</translation>
     </message>
@@ -979,19 +1022,6 @@ Median koko: %2
         <translation>Kuvan lähetys epäonnistui: %s</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1015,7 +1045,7 @@ Median koko: %2
         <translation>Aloita keskustelu</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation>Laitteet</translation>
     </message>
@@ -1066,69 +1096,103 @@ Median koko: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
+        <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
+        <source>You sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>All Rooms</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
-        <translation>Sinä</translation>
-    </message>
-    <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1148,7 +1212,7 @@ Median koko: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts
index 42f82b0fcb59c86a01a04f3089e30847286e5c89..8ef2226822ed8831b683b970782caf68bde918cf 100644
--- a/resources/langs/nheko_fr.ts
+++ b/resources/langs/nheko_fr.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="fr">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>Enregistrer le fichier</translation>
-    </message>
-</context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. 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>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+380"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -42,18 +19,18 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Enregistrer le fichier</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Enregistrer l&apos;image</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Membres du salon</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -278,7 +268,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -286,12 +276,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Quitter le salon</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Accepter</translation>
     </message>
@@ -332,36 +322,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
+        <location line="+1"/>
+        <source>Sent</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
+        <location line="+1"/>
+        <source>Received</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
+        <location line="+1"/>
+        <source>Read</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -392,65 +382,113 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
+    <name>TimelineModel</name>
+    <message>
+        <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>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>-- Decryption Error (failed to communicate with DB) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+19"/>
+        <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>-- Decryption Error (%1) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
+        <location line="+25"/>
+        <source>-- Encrypted Event (Unknown event type) --</source>
+        <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+39"/>
-        <source>Reply</source>
+        <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="+11"/>
-        <source>Options</source>
+        <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>TimelineView</name>
+    <name>TimelineRow</name>
     <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+65"/>
-        <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>
+        <location line="+14"/>
+        <source>Options</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
-        <source>-- Decryption Error (failed to communicate with DB) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Accusés de lecture</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
-        <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
+        <location line="+3"/>
+        <source>View raw message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
-        <source>-- Decryption Error (%1) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
+        <location line="+4"/>
+        <source>Redact message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
-        <source>-- Encrypted Event (Unknown event type) --</source>
-        <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -516,7 +554,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Réduire à la barre des tâches</translation>
     </message>
@@ -530,6 +568,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>Barre latérale des groupes</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -606,7 +649,7 @@
         <translation>GÉNÉRAL</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
@@ -826,7 +869,7 @@ Taille du média : %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>Accusés de lecture</translation>
     </message>
@@ -952,7 +995,7 @@ Taille du média : %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -978,19 +1021,6 @@ Taille du média : %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1014,7 +1044,7 @@ Taille du média : %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1065,69 +1095,103 @@ Taille du média : %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
+        <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
+        <source>You sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
+        <location line="+1"/>
+        <source>All Rooms</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>utils</name>
     <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1147,7 +1211,7 @@ Taille du média : %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts
index 53840f8244ed72f90c5ddc13bf4153dd95fc687e..aaeae41c242c3a73e928cf187f0970582f69f88e 100644
--- a/resources/langs/nheko_nl.ts
+++ b/resources/langs/nheko_nl.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="nl_NL">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>Bestand opslaan</translation>
-    </message>
-</context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+330"/>
-        <source>Failed to upload image. 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>
+        <location filename="../../src/ChatPage.cpp" line="+346"/>
+        <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+380"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -42,18 +19,18 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Bestand opslaan</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Afbeelding opslaan</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Kamerleden</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Kamer verlaten</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Accepteren</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
+        <location line="+1"/>
+        <source>Sent</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
+        <location line="+1"/>
+        <source>Received</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
+        <location line="+1"/>
+        <source>Read</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -391,65 +381,113 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
+    <name>TimelineModel</name>
+    <message>
+        <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>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>-- Decryption Error (failed to communicate with DB) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+19"/>
+        <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>-- Decryption Error (%1) --</source>
+        <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
+        <location line="+25"/>
+        <source>-- Encrypted Event (Unknown event type) --</source>
+        <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+39"/>
-        <source>Reply</source>
+        <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="+11"/>
-        <source>Options</source>
+        <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>TimelineView</name>
+    <name>TimelineRow</name>
     <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+65"/>
-        <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>
+        <location line="+14"/>
+        <source>Options</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
-        <source>-- Decryption Error (failed to communicate with DB) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed when trying to lookup the session.</comment>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Leesbevestigingen</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
-        <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted, because the DB access failed.</comment>
+        <location line="+3"/>
+        <source>View raw message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
-        <source>-- Decryption Error (%1) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment>
+        <location line="+4"/>
+        <source>Redact message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
-        <source>-- Encrypted Event (Unknown event type) --</source>
-        <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -515,7 +553,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Minimaliseren naar systeemvak</translation>
     </message>
@@ -529,6 +567,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>Zijbalk van groep</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -605,7 +648,7 @@
         <translation>ALGEMEEN</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
@@ -825,7 +868,7 @@ Mediagrootte: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>Leesbevestigingen</translation>
     </message>
@@ -951,7 +994,7 @@ Mediagrootte: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -977,19 +1020,6 @@ Mediagrootte: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1013,7 +1043,7 @@ Mediagrootte: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1064,69 +1094,103 @@ Mediagrootte: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
+        <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
+        <source>You sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
+        <location line="+1"/>
+        <source>All Rooms</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>utils</name>
     <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1146,7 +1210,7 @@ Mediagrootte: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts
index f4f98dbbc72f946bd095189ebb48489e73d76f12..b7c3878d9fc97dfcd25a5da6258fc2f0b7fd7d77 100644
--- a/resources/langs/nheko_pl.ts
+++ b/resources/langs/nheko_pl.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="pl">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>Zapisz plik</translation>
-    </message>
-</context>
 <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="+380"/>
+        <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>
@@ -42,18 +19,18 @@
         <translation>Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie.</translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation>Spróbuj zalogować się ponownie: %1</translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation>Tworzenie pokoju nie powiodło się: %1</translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Zapisz plik</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Zapisz obraz</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Członkowie pokoju</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Opuść pokój</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Akceptuj</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
-        <translation>Szyfrowana</translation>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
-        <translation>Dostarczono</translation>
+        <location line="+1"/>
+        <source>Sent</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
-        <translation>Wyświetlona</translation>
+        <location line="+1"/>
+        <source>Received</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
-        <translation>Wysłana</translation>
+        <location line="+1"/>
+        <source>Read</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -391,32 +381,9 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
-    <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
-        <source>Message redaction failed: %1</source>
-        <translation>Redagowanie wiadomości nie powiodło się: %1</translation>
-    </message>
-    <message>
-        <location line="+39"/>
-        <source>Reply</source>
-        <translation type="unfinished"></translation>
-    </message>
+    <name>TimelineModel</name>
     <message>
-        <location line="+11"/>
-        <source>Options</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>TimelineView</name>
-    <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
-        <translation>Szyfrowanie jest włączone</translation>
-    </message>
-    <message>
-        <location line="+65"/>
+        <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>
@@ -440,16 +407,87 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+25"/>
         <source>-- Encrypted Event (Unknown event type) --</source>
         <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <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>
+    <message>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Options</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Potwierdzenia przeczytania</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>View raw message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Redact message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation>Ustawienia pokoju</translation>
     </message>
@@ -516,7 +554,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Zminimalizuj do paska zadań</translation>
     </message>
@@ -530,6 +568,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>Pasek boczny grupy</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -606,7 +649,7 @@
         <translation>OGÓLNE</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
@@ -826,7 +869,7 @@ Rozmiar multimediów: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>Potwierdzenia przeczytania</translation>
     </message>
@@ -955,7 +998,7 @@ Rozmiar multimediów: %2
         <translation>Nie udało się włączyć szyfrowania: %1</translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation>Wybierz awatar</translation>
     </message>
@@ -981,19 +1024,6 @@ Rozmiar multimediów: %2
         <translation>Nie udało się wysłać obrazu: %s</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1017,7 +1047,7 @@ Rozmiar multimediów: %2
         <translation>Rozpocznij rozmowÄ™</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation>UrzÄ…dzenia</translation>
     </message>
@@ -1068,69 +1098,103 @@ Rozmiar multimediów: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
+        <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
+        <source>You sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
+        <location line="+1"/>
+        <source>All Rooms</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>utils</name>
     <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1150,7 +1214,7 @@ Rozmiar multimediów: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts
index 04285c7298c9f853132b31fa9b681f2c3fb71759..3069cdadb3347b81f9199e33e65e13519825015b 100644
--- a/resources/langs/nheko_ru.ts
+++ b/resources/langs/nheko_ru.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="ru">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>Сохранить файл</translation>
-    </message>
-</context>
 <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="+380"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.</translation>
     </message>
@@ -42,18 +19,18 @@
         <translation>Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова.</translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже.</translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation>Повторите попытку входа: %1</translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation>Не удалось создать комнату: %1</translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>Сохранить файл</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>Сохранить изображение</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>Участники комнаты</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>Покинуть комнату</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>Принять</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
-        <translation>Зашифровано</translation>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
-        <translation>Доставлено</translation>
+        <location line="+1"/>
+        <source>Sent</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
-        <translation>Прочитано</translation>
+        <location line="+1"/>
+        <source>Received</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
-        <translation>Отправлено</translation>
+        <location line="+1"/>
+        <source>Read</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -391,32 +381,9 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
-    <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
-        <source>Message redaction failed: %1</source>
-        <translation>Ошибка редактирования сообщения: %1</translation>
-    </message>
-    <message>
-        <location line="+39"/>
-        <source>Reply</source>
-        <translation type="unfinished"></translation>
-    </message>
+    <name>TimelineModel</name>
     <message>
-        <location line="+11"/>
-        <source>Options</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>TimelineView</name>
-    <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
-        <translation>Шифрование включено</translation>
-    </message>
-    <message>
-        <location line="+65"/>
+        <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>
@@ -440,16 +407,87 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+25"/>
         <source>-- Encrypted Event (Unknown event type) --</source>
         <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <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>
+    <message>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Options</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Подтверждать прочтение</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>View raw message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Redact message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation>Настройки комнаты</translation>
     </message>
@@ -516,7 +554,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>Сворачивать в системную панель</translation>
     </message>
@@ -530,6 +568,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>Боковая панель групп</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -606,7 +649,7 @@
         <translation>ГЛАВНОЕ</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation>Открыть файл сеансов</translation>
     </message>
@@ -827,7 +870,7 @@ Media size: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>Подтверждать прочтение</translation>
     </message>
@@ -954,7 +997,7 @@ Media size: %2
         <translation>Не удалось включить шифрование: %1</translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation>Выберите аватар</translation>
     </message>
@@ -980,19 +1023,6 @@ Media size: %2
         <translation>Не удалось загрузить изображение: %s</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1016,7 +1046,7 @@ Media size: %2
         <translation>Начать разговор</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation>Устройства</translation>
     </message>
@@ -1067,69 +1097,103 @@ Media size: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
+        <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
+        <source>You sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
+        <location line="+1"/>
+        <source>All Rooms</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>utils</name>
     <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1149,7 +1213,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts
index 1e539e64f75d2a2a418e1349e6b642a1a68ab29a..31ca068cdfb9d48e64d89a575ad3d34719c453ed 100644
--- a/resources/langs/nheko_zh_CN.ts
+++ b/resources/langs/nheko_zh_CN.ts
@@ -1,38 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE TS>
 <TS version="2.1" language="zh_CN">
-<context>
-    <name>AudioItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/>
-        <source>Save File</source>
-        <translation>保存文件</translation>
-    </message>
-</context>
 <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="+380"/>
+        <location line="+389"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>恢复 OLM 账户失败。请重新登录。</translation>
     </message>
@@ -42,18 +19,18 @@
         <translation>恢复保存的数据失败。请重新登录。</translation>
     </message>
     <message>
-        <location line="+198"/>
+        <location line="+181"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+51"/>
-        <location line="+153"/>
+        <location line="+155"/>
         <source>Please try to login again: %1</source>
         <translation>请尝试再次登录:%1</translation>
     </message>
     <message>
-        <location line="-45"/>
+        <location line="-47"/>
         <source>Room creation failed: %1</source>
         <translation>创建聊天室失败:%1</translation>
     </message>
@@ -116,19 +93,11 @@
     </message>
 </context>
 <context>
-    <name>FileItem</name>
+    <name>EncryptionIndicator</name>
     <message>
-        <location filename="../../src/timeline/widgets/FileItem.cpp" line="+107"/>
-        <source>Save File</source>
-        <translation>保存文件</translation>
-    </message>
-</context>
-<context>
-    <name>ImageItem</name>
-    <message>
-        <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+241"/>
-        <source>Save image</source>
-        <translation>保存图像</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+11"/>
+        <source>Encrypted</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -200,7 +169,7 @@
 <context>
     <name>MemberList</name>
     <message>
-        <location filename="../../src/dialogs/MemberList.cpp" line="+96"/>
+        <location filename="../../src/dialogs/MemberList.cpp" line="+89"/>
         <source>Room members</source>
         <translation>聊天室成员</translation>
     </message>
@@ -210,6 +179,27 @@
         <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>
+        <location filename="../qml/delegates/Placeholder.qml" line="+4"/>
+        <source>unimplemented event: </source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>QuickSwitcher</name>
     <message>
@@ -277,7 +267,7 @@
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+2205"/>
+        <location filename="../../src/Cache.cpp" line="+2307"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -285,12 +275,12 @@
 <context>
     <name>RoomInfoListItem</name>
     <message>
-        <location filename="../../src/RoomInfoListItem.cpp" line="+93"/>
+        <location filename="../../src/RoomInfoListItem.cpp" line="+95"/>
         <source>Leave room</source>
         <translation>离开聊天室</translation>
     </message>
     <message>
-        <location line="+181"/>
+        <location line="+161"/>
         <source>Accept</source>
         <translation>接受</translation>
     </message>
@@ -331,36 +321,36 @@
 <context>
     <name>StatusIndicator</name>
     <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+107"/>
-        <source>Encrypted</source>
-        <translation>加密的</translation>
+        <location filename="../qml/StatusIndicator.qml" line="+13"/>
+        <source>Failed</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Delivered</source>
-        <translation>已送达</translation>
+        <location line="+1"/>
+        <source>Sent</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Seen</source>
-        <translation>已阅读</translation>
+        <location line="+1"/>
+        <source>Received</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
-        <source>Sent</source>
-        <translation>已发送</translation>
+        <location line="+1"/>
+        <source>Read</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <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>
@@ -391,32 +381,9 @@
     </message>
 </context>
 <context>
-    <name>TimelineItem</name>
-    <message>
-        <location filename="../../src/timeline/TimelineItem.cpp" line="+85"/>
-        <source>Message redaction failed: %1</source>
-        <translation>删除消息失败:%1</translation>
-    </message>
-    <message>
-        <location line="+39"/>
-        <source>Reply</source>
-        <translation type="unfinished"></translation>
-    </message>
+    <name>TimelineModel</name>
     <message>
-        <location line="+11"/>
-        <source>Options</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>TimelineView</name>
-    <message>
-        <location filename="../../src/timeline/TimelineView.cpp" line="+245"/>
-        <source>Encryption is enabled</source>
-        <translation>加密已启用</translation>
-    </message>
-    <message>
-        <location line="+65"/>
+        <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>
@@ -440,16 +407,87 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+25"/>
         <source>-- Encrypted Event (Unknown event type) --</source>
         <comment>Placeholder, when the message was decrypted, but we couldn&apos;t parse it, because Nheko/mtxclient don&apos;t support that event type yet</comment>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <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>
+    <message>
+        <location filename="../qml/TimelineRow.qml" line="+57"/>
+        <source>Reply</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Options</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">阅读回执</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Mark as read</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>View raw message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Redact message</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Save as</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+24"/>
+        <source>No room open</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>TopRoomBar</name>
     <message>
-        <location filename="../../src/TopRoomBar.cpp" line="+79"/>
+        <location filename="../../src/TopRoomBar.cpp" line="+78"/>
         <source>Room options</source>
         <translation>聊天室选项</translation>
     </message>
@@ -514,7 +552,7 @@
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+166"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+171"/>
         <source>Minimize to tray</source>
         <translation>最小化至托盘</translation>
     </message>
@@ -528,6 +566,11 @@
         <source>Group&apos;s sidebar</source>
         <translation>群组侧边栏</translation>
     </message>
+    <message>
+        <location line="+9"/>
+        <source>Circular Avatars</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+9"/>
         <source>Typing notifications</source>
@@ -604,7 +647,7 @@
         <translation>通用</translation>
     </message>
     <message>
-        <location line="+156"/>
+        <location line="+161"/>
         <source>Open Sessions File</source>
         <translation>打开会话文件</translation>
     </message>
@@ -824,7 +867,7 @@ Media size: %2
 <context>
     <name>dialogs::ReadReceipts</name>
     <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+121"/>
+        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/>
         <source>Read receipts</source>
         <translation>阅读回执</translation>
     </message>
@@ -951,7 +994,7 @@ Media size: %2
         <translation>启用加密失败:%1</translation>
     </message>
     <message>
-        <location line="+149"/>
+        <location line="+148"/>
         <source>Select an avatar</source>
         <translation>选择一个头像</translation>
     </message>
@@ -977,19 +1020,6 @@ Media size: %2
         <translation>上传图像失败:%s</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::UserMentions</name>
-    <message>
-        <location filename="../../src/dialogs/UserMentions.cpp" line="+53"/>
-        <source>This Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>All Rooms</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::UserProfile</name>
     <message>
@@ -1013,7 +1043,7 @@ Media size: %2
         <translation>开始一个聊天</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+56"/>
         <source>Devices</source>
         <translation>设备</translation>
     </message>
@@ -1072,69 +1102,103 @@ Media size: %2
 <context>
     <name>message-description sent:</name>
     <message>
-        <location filename="../../src/Utils.h" line="+104"/>
-        <source>%1 an audio clip</source>
+        <location filename="../../src/Utils.h" line="+95"/>
+        <source>You sent an audio clip</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 an image</source>
+        <source>%1 sent an audio clip</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a file</source>
+        <source>%1 sent a file</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a video</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a video clip</source>
+        <source>%1 sent a sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a sticker</source>
+        <source>%1 sent a notification</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
-        <source>%1 a notification</source>
+        <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+7"/>
-        <source>%1 an encrypted message</source>
+        <source>You sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>message-description:</name>
     <message>
-        <location line="-26"/>
-        <source>sent</source>
-        <comment>For when someone else is the sender</comment>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
-    <name>message-description: </name>
+    <name>popups::UserMentions</name>
     <message>
-        <location line="-2"/>
-        <source>sent</source>
-        <comment>For when you are the sender</comment>
+        <location filename="../../src/popups/UserMentions.cpp" line="+61"/>
+        <source>This Room</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>utils</name>
     <message>
-        <location filename="../../src/Utils.cpp" line="+46"/>
-        <location filename="../../src/Utils.h" line="+55"/>
-        <source>You</source>
+        <location line="+1"/>
+        <source>All Rooms</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>utils</name>
     <message>
-        <location line="+219"/>
+        <location filename="../../src/Utils.cpp" line="+282"/>
         <source>sent a file.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1154,7 +1218,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/Utils.h" line="-23"/>
+        <location filename="../../src/Utils.h" line="+4"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
new file mode 100644
index 0000000000000000000000000000000000000000..a53f057b20d1be4c05efdb11c96a4ceeae82bc7b
--- /dev/null
+++ b/resources/qml/Avatar.qml
@@ -0,0 +1,51 @@
+import QtQuick 2.6
+import QtGraphicalEffects 1.0
+import Qt.labs.settings 1.0
+
+Rectangle {
+	id: avatar
+	width: 48
+	height: 48
+	radius: settings.avatar_circles ? height/2 : 3
+
+	Settings {
+		id: settings
+		category: "user"
+		property bool avatar_circles: true
+	}
+
+	property alias url: img.source
+	property string displayName
+
+	Text {
+		anchors.fill: parent
+		text: String.fromCodePoint(displayName.codePointAt(0))
+		color: colors.text
+		font.pixelSize: avatar.height/2
+		verticalAlignment: Text.AlignVCenter
+		horizontalAlignment: Text.AlignHCenter
+	}
+
+	Image {
+		id: img
+		anchors.fill: parent
+		asynchronous: true
+		fillMode: Image.PreserveAspectCrop
+		mipmap: true
+		smooth: false
+
+		sourceSize.width: avatar.width
+		sourceSize.height: avatar.height
+
+		layer.enabled: true
+		layer.effect: OpacityMask {
+			maskSource: Rectangle {
+				anchors.fill: parent
+				width: avatar.width
+				height: avatar.height
+				radius: settings.avatar_circles ? height/2 : 3
+			}
+		}
+	}
+	color: colors.dark
+}
diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml
new file mode 100644
index 0000000000000000000000000000000000000000..905cf9346ac34e2fb783e7ff664229451e9d68e2
--- /dev/null
+++ b/resources/qml/EncryptionIndicator.qml
@@ -0,0 +1,24 @@
+import QtQuick 2.5
+import QtQuick.Controls 2.1
+import im.nheko 1.0
+
+Rectangle {
+	id: indicator
+	color: "transparent"
+	width: 16
+	height: 16
+	ToolTip.visible: ma.containsMouse && indicator.visible
+	ToolTip.text: qsTr("Encrypted")
+	MouseArea{
+		id: ma
+		anchors.fill: parent
+		hoverEnabled: true
+	}
+
+	Image {
+		id: stateImg
+		anchors.fill: parent
+		source: "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText
+	}
+}
+
diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml
new file mode 100644
index 0000000000000000000000000000000000000000..dc576e183eb9aebddac9e912bc393a8118f92d82
--- /dev/null
+++ b/resources/qml/ImageButton.qml
@@ -0,0 +1,29 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.3
+
+Button {
+	property string image: undefined
+
+	id: button
+
+	flat: true
+
+	// disable background, because we don't want a border on hover
+	background: Item {
+	}
+
+	Image {
+		id: buttonImg
+		// Workaround, can't get icon.source working for now...
+		anchors.fill: parent
+		source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText)
+	}
+
+	MouseArea
+	{
+		id: mouseArea
+		anchors.fill: parent
+		onPressed:  mouse.accepted = false
+		cursorShape: Qt.PointingHandCursor
+	}
+}
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
new file mode 100644
index 0000000000000000000000000000000000000000..46e7471179b13ad15fbae1fc2888f9686efbfed1
--- /dev/null
+++ b/resources/qml/MatrixText.qml
@@ -0,0 +1,33 @@
+import QtQuick 2.5
+import QtQuick.Controls 2.3
+
+TextEdit {
+	textFormat: TextEdit.RichText
+	readOnly: true
+	wrapMode: Text.Wrap
+	selectByMouse: true
+	color: colors.text
+
+	onLinkActivated: {
+		if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1])
+		else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1])
+		else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
+			var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link)
+			timelineManager.setHistoryView(match[1])
+			chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain)
+		}
+		else Qt.openUrlExternally(link)
+	}
+	MouseArea
+	{
+		anchors.fill: parent
+		onPressed:  mouse.accepted = false
+		cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
+	}
+
+	ToolTip {
+		visible: parent.hoveredLink
+		text: parent.hoveredLink
+		palette: colors
+	}
+}
diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml
new file mode 100644
index 0000000000000000000000000000000000000000..91e8f76990f146a82ffc8326d131f65dd61c0f22
--- /dev/null
+++ b/resources/qml/StatusIndicator.qml
@@ -0,0 +1,38 @@
+import QtQuick 2.5
+import QtQuick.Controls 2.1
+import im.nheko 1.0
+
+Rectangle {
+	id: indicator
+	property int state: 0
+	color: "transparent"
+	width: 16
+	height: 16
+	ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty
+	ToolTip.text: switch (state) {
+		case MtxEvent.Failed: return qsTr("Failed")
+		case MtxEvent.Sent: return qsTr("Sent")
+		case MtxEvent.Received: return qsTr("Received")
+		case MtxEvent.Read: return qsTr("Read")
+		default: return ""
+	}
+	MouseArea{
+		id: ma
+		anchors.fill: parent
+		hoverEnabled: true
+	}
+
+	Image {
+		id: stateImg
+		// Workaround, can't get icon.source working for now...
+		anchors.fill: parent
+		source: switch (indicator.state) {
+			case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText
+			case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText
+			case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText
+			case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText
+			default: return ""
+		}
+	}
+}
+
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
new file mode 100644
index 0000000000000000000000000000000000000000..2c2ed02ad614fd90ce642e392aff7a45a51395d0
--- /dev/null
+++ b/resources/qml/TimelineRow.qml
@@ -0,0 +1,122 @@
+import QtQuick 2.6
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.2
+
+import im.nheko 1.0
+
+import "./delegates"
+
+RowLayout {
+	property var view: chat
+
+	anchors.leftMargin: avatarSize + 4
+	anchors.left: parent.left
+	anchors.right: parent.right
+
+	height: Math.max(contentItem.height, 16)
+
+	Column {
+		Layout.fillWidth: true
+		Layout.alignment: Qt.AlignTop
+
+		//property var replyTo: model.replyTo
+
+		//Text {
+		//	property int idx: timelineManager.timeline.idToIndex(replyTo)
+		//	text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing")
+		//}
+		MessageDelegate {
+			id: contentItem
+
+			width: parent.width
+			height: childrenRect.height
+		}
+	}
+
+	StatusIndicator {
+		state: model.state
+		Layout.alignment: Qt.AlignRight | Qt.AlignTop
+		Layout.preferredHeight: 16
+	}
+
+	EncryptionIndicator {
+		visible: model.isEncrypted
+		Layout.alignment: Qt.AlignRight | Qt.AlignTop
+		Layout.preferredHeight: 16
+	}
+
+	ImageButton {
+		Layout.alignment: Qt.AlignRight | Qt.AlignTop
+		Layout.preferredHeight: 16
+		id: replyButton
+
+		image: ":/icons/icons/ui/mail-reply.png"
+		ToolTip {
+			visible: replyButton.hovered
+			text: qsTr("Reply")
+			palette: colors
+		}
+
+		onClicked: view.model.replyAction(model.id)
+	}
+	ImageButton {
+		Layout.alignment: Qt.AlignRight | Qt.AlignTop
+		Layout.preferredHeight: 16
+		id: optionsButton
+
+		image: ":/icons/icons/ui/vertical-ellipsis.png"
+		ToolTip {
+			visible: optionsButton.hovered
+			text: qsTr("Options")
+			palette: colors
+		}
+
+		onClicked: contextMenu.open()
+
+		Menu {
+			y: optionsButton.height
+			id: contextMenu
+			palette: colors
+
+			MenuItem {
+				text: qsTr("Read receipts")
+				onTriggered: view.model.readReceiptsAction(model.id)
+			}
+			MenuItem {
+				text: qsTr("Mark as read")
+			}
+			MenuItem {
+				text: qsTr("View raw message")
+				onTriggered: view.model.viewRawMessage(model.id)
+			}
+			MenuItem {
+				text: qsTr("Redact message")
+				onTriggered: view.model.redactEvent(model.id)
+			}
+			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.timeline.saveMedia(model.id)
+			}
+		}
+	}
+
+	Text {
+		Layout.alignment: Qt.AlignRight | Qt.AlignTop
+		text: model.timestamp.toLocaleTimeString("HH:mm")
+		color: inactiveColors.text
+
+		MouseArea{
+			id: ma
+			anchors.fill: parent
+			hoverEnabled: true
+		}
+
+		ToolTip {
+			visible: ma.containsMouse
+			text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
+			palette: colors
+		}
+	}
+}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
new file mode 100644
index 0000000000000000000000000000000000000000..1a1900ad472e821138df5790558e5a3a894ee87f
--- /dev/null
+++ b/resources/qml/TimelineView.qml
@@ -0,0 +1,185 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import QtGraphicalEffects 1.0
+import QtQuick.Window 2.2
+
+import im.nheko 1.0
+
+import "./delegates"
+
+Item {
+	property var colors: currentActivePalette
+	property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled }
+	property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
+	property int avatarSize: 40
+
+	Rectangle {
+		anchors.fill: parent
+		color: colors.window
+
+		Text {
+			visible: !timelineManager.timeline && !timelineManager.isInitialSync
+			anchors.centerIn: parent
+			text: qsTr("No room open")
+			font.pointSize: 24
+			color: colors.windowText
+		}
+
+		BusyIndicator {
+			anchors.centerIn: parent
+			running: timelineManager.isInitialSync
+			height: 200
+			width: 200
+		}
+
+		ListView {
+			id: chat
+
+			cacheBuffer: 2000
+
+			visible: timelineManager.timeline != null
+			anchors.fill: parent
+
+			anchors.leftMargin: 4
+			anchors.rightMargin: scrollbar.width
+
+			model: timelineManager.timeline
+
+			boundsBehavior: Flickable.StopAtBounds
+
+			onVerticalOvershootChanged: contentY = contentY - verticalOvershoot
+
+			MouseArea {
+				anchors.fill: parent
+				acceptedButtons: Qt.NoButton
+				propagateComposedEvents: true
+				z: -1
+				onWheel: {
+					if (wheel.angleDelta != 0) {
+						chat.contentY = chat.contentY - wheel.angleDelta.y
+						wheel.accepted = true
+						chat.forceLayout()
+						chat.updatePosition()
+					}
+				}
+			}
+
+			onModelChanged: {
+				if (model) {
+					currentIndex = model.currentIndex
+					if (model.currentIndex == count - 1) {
+						positionViewAtEnd()
+					} else {
+						positionViewAtIndex(model.currentIndex, ListView.End)
+					}
+				}
+			}
+
+			ScrollBar.vertical: ScrollBar {
+				id: scrollbar
+				parent: chat.parent
+				anchors.top: chat.top
+				anchors.left: chat.right
+				anchors.bottom: chat.bottom
+				onPressedChanged: if (!pressed) chat.updatePosition()
+			}
+
+			property bool atBottom: false
+			onCountChanged: {
+				if (atBottom) {
+					var newIndex = count - 1 // last index
+					positionViewAtEnd()
+					currentIndex = newIndex
+					model.currentIndex = newIndex
+				}
+
+				if (contentHeight < height && model) {
+					model.fetchHistory();
+				}
+			}
+
+			onAtYBeginningChanged: if (atYBeginning) { chat.model.currentIndex = 0; chat.currentIndex = 0; model.fetchHistory(); }
+
+			function updatePosition() {
+				for (var y = chat.contentY + chat.height; y > chat.height; y -= 9) {
+					var i = chat.itemAt(100, y);
+					if (!i) continue;
+					if (!i.isFullyVisible()) continue;
+					chat.model.currentIndex = i.getIndex();
+					chat.currentIndex = i.getIndex()
+					atBottom = i.getIndex() == count - 1;
+					break;
+				}
+			}
+			onMovementEnded: updatePosition()
+
+			spacing: 4
+			delegate: TimelineRow {
+				function isFullyVisible() {
+					return height > 1 && (y - chat.contentY - 1) + height < chat.height
+				}
+				function getIndex() {
+					return index;
+				}
+			}
+
+			section {
+				property: "section"
+				delegate: Column {
+					topPadding: 4
+					bottomPadding: 4
+					spacing: 8
+
+					width: parent.width
+					height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
+
+					Label {
+						id: dateBubble
+						anchors.horizontalCenter: parent.horizontalCenter
+						visible: section.includes(" ")
+						text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1])))
+						color: colors.windowText
+
+						height: contentHeight * 1.2
+						width: contentWidth * 1.2
+						horizontalAlignment: Text.AlignHCenter
+						background: Rectangle {
+							radius: parent.height / 2
+							color: colors.dark
+						}
+					}
+					Row {
+						height: userName.height
+						spacing: 4
+						Avatar {
+							width: avatarSize
+							height: avatarSize
+							url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/")
+							displayName: chat.model.displayName(section.split(" ")[0])
+
+							MouseArea {
+								anchors.fill: parent
+								onClicked: chat.model.openUserProfile(section.split(" ")[0])
+								cursorShape: Qt.PointingHandCursor
+							}
+						}
+
+						Text { 
+							id: userName
+							text: chat.model.escapeEmoji(chat.model.displayName(section.split(" ")[0]))
+							color: chat.model.userColor(section.split(" ")[0], colors.window)
+							textFormat: Text.RichText
+
+							MouseArea {
+								anchors.fill: parent
+								onClicked: chat.model.openUserProfile(section.split(" ")[0])
+								cursorShape: Qt.PointingHandCursor
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..2c911c5ee1cb3359d2cfecfc5f614dfdfa83b7db
--- /dev/null
+++ b/resources/qml/delegates/FileMessage.qml
@@ -0,0 +1,57 @@
+import QtQuick 2.6
+import QtQuick.Layouts 1.2
+
+Rectangle {
+	radius: 10
+	color: colors.dark
+	height: row.height + 24
+	width: parent ? parent.width : undefined
+
+	RowLayout {
+		id: row
+
+		anchors.centerIn: parent
+		width: parent.width - 24
+
+		spacing: 15
+
+		Rectangle {
+			id: button
+			color: colors.light
+			radius: 22
+			height: 44
+			width: 44
+			Image {
+				id: img
+				anchors.centerIn: parent
+
+				source: "qrc:/icons/icons/ui/arrow-pointing-down.png"
+				fillMode: Image.Pad
+
+			}
+			MouseArea {
+				anchors.fill: parent
+				onClicked: timelineManager.timeline.saveMedia(model.id)
+				cursorShape: Qt.PointingHandCursor
+			}
+		}
+		ColumnLayout {
+			id: col
+
+			Text {
+				Layout.fillWidth: true
+				text: model.body
+				textFormat: Text.PlainText
+				elide: Text.ElideRight
+				color: colors.text
+			}
+			Text {
+				Layout.fillWidth: true
+				text: model.filesize
+				textFormat: Text.PlainText
+				elide: Text.ElideRight
+				color: colors.text
+			}
+		}
+	}
+}
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..1b6e572983825634d71f5a1c81a499f1f021dfa2
--- /dev/null
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -0,0 +1,23 @@
+import QtQuick 2.6
+
+import im.nheko 1.0
+
+Item {
+	width: Math.min(parent ? parent.width : undefined, model.width)
+	height: width * model.proportionalHeight
+
+	Image {
+		id: img
+		anchors.fill: parent
+
+		source: model.url.replace("mxc://", "image://MxcImage/")
+		asynchronous: true
+		fillMode: Image.PreserveAspectFit
+
+		MouseArea {
+			enabled: model.type == MtxEvent.ImageMessage
+			anchors.fill: parent
+			onClicked: timelineManager.openImageOverlay(model.url, model.id)
+		}
+	}
+}
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
new file mode 100644
index 0000000000000000000000000000000000000000..178dfd86054091dee556b9d66e47fd529c0e478d
--- /dev/null
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -0,0 +1,55 @@
+import QtQuick 2.6
+import im.nheko 1.0
+
+DelegateChooser {
+	//role: "type" //< not supported in our custom implementation, have to use roleValue
+	roleValue: model.type
+
+	DelegateChoice {
+		roleValue: MtxEvent.TextMessage
+		TextMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.NoticeMessage
+		NoticeMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.EmoteMessage
+		TextMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.ImageMessage
+		ImageMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.Sticker
+		ImageMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.FileMessage
+		FileMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.VideoMessage
+		PlayableMediaMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.AudioMessage
+		PlayableMediaMessage {}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.Redacted
+		Pill {
+			text: qsTr("redacted")
+		}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.Encryption
+		Pill {
+			text: qsTr("Encryption enabled")
+		}
+	}
+	DelegateChoice {
+		Placeholder {}
+	}
+}
diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..a392eb5bb418ac0dd0ed456aeeff104f803be757
--- /dev/null
+++ b/resources/qml/delegates/NoticeMessage.qml
@@ -0,0 +1,8 @@
+import ".."
+
+MatrixText {
+	text: model.formattedBody
+	width: parent ? parent.width : undefined
+	font.italic: true
+	color: inactiveColors.text
+}
diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml
new file mode 100644
index 0000000000000000000000000000000000000000..53a9684e4949223648422ab028353c26a45f5716
--- /dev/null
+++ b/resources/qml/delegates/Pill.qml
@@ -0,0 +1,14 @@
+import QtQuick 2.5
+import QtQuick.Controls 2.1
+
+Label {
+	color: inactiveColors.text
+	horizontalAlignment: Text.AlignHCenter
+
+	height: contentHeight * 1.2
+	width: contentWidth * 1.2
+	background: Rectangle {
+		radius: parent.height / 2
+		color: colors.dark
+	}
+}
diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml
new file mode 100644
index 0000000000000000000000000000000000000000..4c0e68c39b0db3e9b601d1efae00814a9999f88c
--- /dev/null
+++ b/resources/qml/delegates/Placeholder.qml
@@ -0,0 +1,7 @@
+import ".."
+
+MatrixText {
+	text: qsTr("unimplemented event: ") + model.type
+	width: parent ? parent.width : undefined
+	color: inactiveColors.text
+}
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..d0d4d7cb7dd23413f356af2eb18e9fc8644bac30
--- /dev/null
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -0,0 +1,164 @@
+import QtQuick 2.6
+import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.1
+import QtMultimedia 5.6
+
+import im.nheko 1.0
+
+Rectangle {
+	id: bg
+	radius: 10
+	color: colors.dark
+	height: content.height + 24
+	width: parent ? parent.width : undefined
+
+	Column { 
+		id: content
+		width: parent.width - 24
+		anchors.centerIn: parent
+
+		Rectangle {
+			id: videoContainer
+			visible: model.type == MtxEvent.VideoMessage
+			width: Math.min(parent.width, model.width ? model.width : 400) // some media has 0 as size...
+			height: width*model.proportionalHeight
+			Image {
+				anchors.fill: parent
+				source: model.thumbnailUrl.replace("mxc://", "image://MxcImage/")
+				asynchronous: true
+				fillMode: Image.PreserveAspectFit
+
+				VideoOutput {
+					anchors.fill: parent
+					fillMode: VideoOutput.PreserveAspectFit
+					source: media
+				}
+			}
+		}
+
+		RowLayout {
+			width: parent.width
+			Text {
+				id: positionText
+				text: "--:--:--"
+				color: colors.text
+			}
+			Slider {
+				Layout.fillWidth: true
+				id: progress
+				value: media.position
+				from: 0
+				to: media.duration
+
+				onMoved: media.seek(value)
+				//indeterminate: true
+				function updatePositionTexts() {
+					function formatTime(date) {
+						var hh = date.getUTCHours();
+						var mm = date.getUTCMinutes();
+						var ss = date.getSeconds();
+						if (hh < 10) {hh = "0"+hh;}
+						if (mm < 10) {mm = "0"+mm;}
+						if (ss < 10) {ss = "0"+ss;}
+						return hh+":"+mm+":"+ss;
+					}
+					positionText.text = formatTime(new Date(media.position))
+					durationText.text = formatTime(new Date(media.duration))
+				}
+				onValueChanged: updatePositionTexts()
+			}
+			Text {
+				id: durationText
+				text: "--:--:--"
+				color: colors.text
+			}
+		}
+
+		RowLayout {
+			width: parent.width
+
+			spacing: 15
+
+			Rectangle {
+				id: button
+				color: colors.light
+				radius: 22
+				height: 44
+				width: 44
+				Image {
+					id: img
+					anchors.centerIn: parent
+
+					source: "qrc:/icons/icons/ui/arrow-pointing-down.png"
+					fillMode: Image.Pad
+
+				}
+				MouseArea {
+					anchors.fill: parent
+					onClicked: {
+						switch (button.state) {
+							case "": timelineManager.timeline.cacheMedia(model.id); break;
+							case "stopped":
+							media.play(); console.log("play");
+							button.state = "playing"
+							break
+							case "playing":
+							media.pause(); console.log("pause");
+							button.state = "stopped"
+							break
+						}
+					}
+					cursorShape: Qt.PointingHandCursor
+				}
+				MediaPlayer {
+					id: media
+					onError: console.log(errorString)
+					onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts()
+					onStopped: button.state = "stopped"
+				}
+
+				Connections {
+					target: timelineManager.timeline
+					onMediaCached: {
+						if (mxcUrl == model.url) {
+							media.source = "file://" + cacheUrl
+							button.state = "stopped"
+							console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
+						}
+						console.log("media cached: " + mxcUrl + " at " + cacheUrl)
+					}
+				}
+
+				states: [
+					State {
+						name: "stopped"
+						PropertyChanges { target: img; source: "qrc:/icons/icons/ui/play-sign.png" }
+					},
+					State {
+						name: "playing"
+						PropertyChanges { target: img; source: "qrc:/icons/icons/ui/pause-symbol.png" }
+					}
+				]
+			}
+			ColumnLayout {
+				id: col
+
+				Text {
+					Layout.fillWidth: true
+					text: model.body
+					textFormat: Text.PlainText
+					elide: Text.ElideRight
+					color: colors.text
+				}
+				Text {
+					Layout.fillWidth: true
+					text: model.filesize
+					textFormat: Text.PlainText
+					elide: Text.ElideRight
+					color: colors.text
+				}
+			}
+		}
+	}
+}
+
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..f984b32f17428ad98de773c1eb69beaf48243d83
--- /dev/null
+++ b/resources/qml/delegates/TextMessage.qml
@@ -0,0 +1,6 @@
+import ".."
+
+MatrixText {
+	text: model.formattedBody.replace("<pre>", "<pre style='white-space: pre-wrap'>")
+	width: parent ? parent.width : undefined
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index ad27af5a437104533daa93b02f7cccf16b8523e0..53406c48ee73168e620524e7784f7a4f95e4701e 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -114,4 +114,21 @@
         <file>styles/nheko.qss</file>
         <file>styles/nheko-dark.qss</file>
     </qresource>
+    <qresource prefix="/">
+        <file>qml/TimelineView.qml</file>
+        <file>qml/Avatar.qml</file>
+        <file>qml/ImageButton.qml</file>
+        <file>qml/MatrixText.qml</file>
+        <file>qml/StatusIndicator.qml</file>
+        <file>qml/EncryptionIndicator.qml</file>
+        <file>qml/TimelineRow.qml</file>
+        <file>qml/delegates/MessageDelegate.qml</file>
+        <file>qml/delegates/TextMessage.qml</file>
+        <file>qml/delegates/NoticeMessage.qml</file>
+        <file>qml/delegates/ImageMessage.qml</file>
+        <file>qml/delegates/PlayableMediaMessage.qml</file>
+        <file>qml/delegates/FileMessage.qml</file>
+        <file>qml/delegates/Pill.qml</file>
+        <file>qml/delegates/Placeholder.qml</file>
+    </qresource>
 </RCC>
diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp
index ec745c049f25475c7fe524e5002e3cf7a8925c6f..68b6901eedd00a9b77cb694a7cf1ce91745ab27d 100644
--- a/src/AvatarProvider.cpp
+++ b/src/AvatarProvider.cpp
@@ -43,7 +43,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
 
         QPixmap pixmap;
         if (avatar_cache.find(cacheKey, &pixmap)) {
-                nhlog::net()->info("cached pixmap {}", avatarUrl.toStdString());
                 callback(pixmap);
                 return;
         }
@@ -52,7 +51,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
         if (!data.isNull()) {
                 pixmap.loadFromData(data);
                 avatar_cache.insert(cacheKey, pixmap);
-                nhlog::net()->info("loaded pixmap from disk cache {}", avatarUrl.toStdString());
                 callback(pixmap);
                 return;
         }
@@ -69,8 +67,8 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
                          });
 
         mtx::http::ThumbOpts opts;
-        opts.width   = 256;
-        opts.height  = 256;
+        opts.width   = size;
+        opts.height  = size;
         opts.mxc_url = avatarUrl.toStdString();
 
         http::client()->get_thumbnail(
@@ -86,8 +84,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
 
                   cache::client()->saveImage(opts.mxc_url, res);
 
-                  nhlog::net()->info("downloaded pixmap {}", opts.mxc_url);
-
                   emit proxy->avatarDownloaded(QByteArray(res.data(), res.size()));
           });
 }
diff --git a/src/Cache.h b/src/Cache.h
index 0da49793995bc5b3e4c600bef4a0e3c7b6a701bf..f5e1cfa08d32434c90407545abba66398adcb9ca 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -91,7 +91,6 @@ from_json(const json &j, ReadReceiptKey &key)
 struct DescInfo
 {
         QString event_id;
-        QString username;
         QString userid;
         QString body;
         QString timestamp;
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 21ded4b3af4bccf43ead05cf19bd660a2a106618..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);
@@ -113,12 +118,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         view_manager_ = new TimelineViewManager(this);
 
         contentLayout_->addWidget(top_bar_);
-        contentLayout_->addWidget(view_manager_);
-
-        connect(this,
-                &ChatPage::removeTimelineEvent,
-                view_manager_,
-                &TimelineViewManager::removeTimelineEvent);
+        contentLayout_->addWidget(view_manager_->getWidget());
 
         // Splitter
         splitter->addWidget(sideBar_);
@@ -304,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());
 
@@ -316,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,
@@ -327,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);
@@ -566,7 +443,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         connect(this,
                 &ChatPage::initializeViews,
                 view_manager_,
-                [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); });
+                [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); });
         connect(this,
                 &ChatPage::initializeEmptyViews,
                 view_manager_,
@@ -582,7 +459,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                         nhlog::db()->error("failed to retrieve invites: {}", e.what());
                 }
 
-                view_manager_->initialize(rooms);
+                view_manager_->sync(rooms);
                 removeLeftRooms(rooms.leave);
 
                 bool hasNotifications = false;
diff --git a/src/ChatPage.h b/src/ChatPage.h
index e41ae1ae4ac97fb72b1dad96e9db12360335a2fd..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();
@@ -125,8 +114,6 @@ signals:
         void showUserSettingsPage();
         void showOverlayProgressBar();
 
-        void removeTimelineEvent(const QString &room_id, const QString &event_id);
-
         void ownProfileOk();
         void setUserDisplayName(const QString &name);
         void setUserAvatar(const QString &avatar);
diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..92e4732b73c3fe79b92cecedb63375661ea10218
--- /dev/null
+++ b/src/ColorImageProvider.cpp
@@ -0,0 +1,30 @@
+#include "ColorImageProvider.h"
+
+#include "Logging.h"
+#include <QPainter>
+
+QPixmap
+ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &)
+{
+        auto args = id.split('?');
+
+        nhlog::ui()->info("Loading {}, source is {}", id.toStdString(), args[0].toStdString());
+
+        QPixmap source(args[0]);
+
+        if (size)
+                *size = QSize(source.width(), source.height());
+
+        if (args.size() < 2)
+                return source;
+
+        QColor color(args[1]);
+
+        QPixmap colorized = source;
+        QPainter painter(&colorized);
+        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+        painter.fillRect(colorized.rect(), color);
+        painter.end();
+
+        return colorized;
+}
diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h
new file mode 100644
index 0000000000000000000000000000000000000000..21f36c126cd32a6427fa2d25448364cff0cb9ca1
--- /dev/null
+++ b/src/ColorImageProvider.h
@@ -0,0 +1,11 @@
+#include <QQuickImageProvider>
+
+class ColorImageProvider : public QQuickImageProvider
+{
+public:
+        ColorImageProvider()
+          : QQuickImageProvider(QQuickImageProvider::Pixmap)
+        {}
+
+        QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
+};
diff --git a/src/Logging.cpp b/src/Logging.cpp
index 322875826f05af709140ec76288f4e5f7b1e3f4c..126b3781d910eb4712acf9fe5e2e5f28f3d29016 100644
--- a/src/Logging.cpp
+++ b/src/Logging.cpp
@@ -5,14 +5,43 @@
 #include "spdlog/sinks/stdout_color_sinks.h"
 #include <iostream>
 
+#include <QString>
+#include <QtGlobal>
+
 namespace {
 std::shared_ptr<spdlog::logger> db_logger     = nullptr;
 std::shared_ptr<spdlog::logger> net_logger    = nullptr;
 std::shared_ptr<spdlog::logger> crypto_logger = nullptr;
 std::shared_ptr<spdlog::logger> ui_logger     = nullptr;
+std::shared_ptr<spdlog::logger> qml_logger    = nullptr;
 
 constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
 constexpr auto MAX_LOG_FILES = 3;
+
+void
+qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+        std::string localMsg = msg.toStdString();
+        const char *file     = context.file ? context.file : "";
+        const char *function = context.function ? context.function : "";
+        switch (type) {
+        case QtDebugMsg:
+                nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function);
+                break;
+        case QtInfoMsg:
+                nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function);
+                break;
+        case QtWarningMsg:
+                nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function);
+                break;
+        case QtCriticalMsg:
+                nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function);
+                break;
+        case QtFatalMsg:
+                nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function);
+                break;
+        }
+}
 }
 
 namespace nhlog {
@@ -35,12 +64,15 @@ init(const std::string &file_path)
         db_logger  = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
         crypto_logger =
           std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
+        qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks));
 
         if (nheko::enable_debug_log) {
                 db_logger->set_level(spdlog::level::trace);
                 ui_logger->set_level(spdlog::level::trace);
                 crypto_logger->set_level(spdlog::level::trace);
         }
+
+        qInstallMessageHandler(qmlMessageHandler);
 }
 
 std::shared_ptr<spdlog::logger>
@@ -66,4 +98,10 @@ crypto()
 {
         return crypto_logger;
 }
+
+std::shared_ptr<spdlog::logger>
+qml()
+{
+        return qml_logger;
+}
 }
diff --git a/src/Logging.h b/src/Logging.h
index e54f3c3f3b861062858bac8118b6669ab4108372..f572afae0d2a420731dcef7bfb26e921432ae4c2 100644
--- a/src/Logging.h
+++ b/src/Logging.h
@@ -19,5 +19,8 @@ db();
 std::shared_ptr<spdlog::logger>
 crypto();
 
+std::shared_ptr<spdlog::logger>
+qml();
+
 extern bool enable_debug_log_from_commandline;
 }
diff --git a/src/MatrixClient.h b/src/MatrixClient.h
index 2af57267e4d6da20d4b6eb68095ec76a83411139..c77b1183b9cf27ddda4ea4ec2337b2044c7dc858 100644
--- a/src/MatrixClient.h
+++ b/src/MatrixClient.h
@@ -20,16 +20,6 @@ Q_DECLARE_METATYPE(nlohmann::json)
 Q_DECLARE_METATYPE(std::vector<std::string>)
 Q_DECLARE_METATYPE(std::vector<QString>)
 
-class MediaProxy : public QObject
-{
-        Q_OBJECT
-
-signals:
-        void imageDownloaded(const QPixmap &);
-        void imageSaved(const QString &, const QByteArray &);
-        void fileDownloaded(const QByteArray &);
-};
-
 namespace http {
 mtx::http::Client *
 client();
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..edf6ceb5cec2b5f04883616534706e946f339c36
--- /dev/null
+++ b/src/MxcImageProvider.cpp
@@ -0,0 +1,83 @@
+#include "MxcImageProvider.h"
+
+#include "Cache.h"
+
+void
+MxcImageResponse::run()
+{
+        if (m_requestedSize.isValid() && !m_encryptionInfo) {
+                QString fileName = QString("%1_%2x%3_crop")
+                                     .arg(m_id)
+                                     .arg(m_requestedSize.width())
+                                     .arg(m_requestedSize.height());
+
+                auto data = cache::client()->image(fileName);
+                if (!data.isNull() && m_image.loadFromData(data)) {
+                        m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio);
+                        m_image.setText("mxc url", "mxc://" + m_id);
+                        emit finished();
+                        return;
+                }
+
+                mtx::http::ThumbOpts opts;
+                opts.mxc_url = "mxc://" + m_id.toStdString();
+                opts.width   = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1;
+                opts.height  = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1;
+                opts.method  = "crop";
+                http::client()->get_thumbnail(
+                  opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::net()->error("Failed to download image {}",
+                                                      m_id.toStdString());
+                                  m_error = "Failed download";
+                                  emit finished();
+
+                                  return;
+                          }
+
+                          auto data = QByteArray(res.data(), res.size());
+                          cache::client()->saveImage(fileName, data);
+                          m_image.loadFromData(data);
+                          m_image.setText("mxc url", "mxc://" + m_id);
+
+                          emit finished();
+                  });
+        } else {
+                auto data = cache::client()->image(m_id);
+                if (!data.isNull() && m_image.loadFromData(data)) {
+                        m_image.setText("mxc url", "mxc://" + m_id);
+                        emit finished();
+                        return;
+                }
+
+                http::client()->download(
+                  "mxc://" + m_id.toStdString(),
+                  [this](const std::string &res,
+                         const std::string &,
+                         const std::string &originalFilename,
+                         mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::net()->error("Failed to download image {}",
+                                                      m_id.toStdString());
+                                  m_error = "Failed download";
+                                  emit finished();
+
+                                  return;
+                          }
+
+                          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));
+                          m_image.setText("mxc url", "mxc://" + m_id);
+                          cache::client()->saveImage(m_id, data);
+
+                          emit finished();
+                  });
+        }
+}
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
new file mode 100644
index 0000000000000000000000000000000000000000..2c197a13c552fe944be896c6f3c487210007c4e3
--- /dev/null
+++ b/src/MxcImageProvider.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QQuickAsyncImageProvider>
+#include <QQuickImageResponse>
+
+#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,
+                         boost::optional<mtx::crypto::EncryptedFile> encryptionInfo)
+          : m_id(id)
+          , m_requestedSize(requestedSize)
+          , m_encryptionInfo(encryptionInfo)
+        {
+                setAutoDelete(false);
+        }
+
+        QQuickTextureFactory *textureFactory() const override
+        {
+                return QQuickTextureFactory::textureFactoryForImage(m_image);
+        }
+        QString errorString() const override { return m_error; }
+
+        void run() override;
+
+        QString m_id, m_error;
+        QSize m_requestedSize;
+        QImage m_image;
+        boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo;
+};
+
+class MxcImageProvider
+  : public QObject
+  , public QQuickAsyncImageProvider
+{
+        Q_OBJECT
+public slots:
+        QQuickImageResponse *requestImageResponse(const QString &id,
+                                                  const QSize &requestedSize) override
+        {
+                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/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index 8aadbea219ad60797fd55b5c3e6c0d9331a3c1c4..8bebb0f50c46cc669679a09a62c7b058013f9703 100644
--- a/src/RoomInfoListItem.cpp
+++ b/src/RoomInfoListItem.cpp
@@ -118,7 +118,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare
         // so we can't use them for sorting.
         if (roomType_ == RoomType::Invited)
                 lastMsgInfo_ = {
-                  emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
+                  emptyEventId, "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
 }
 
 void
@@ -142,7 +142,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *)
 void
 RoomInfoListItem::paintEvent(QPaintEvent *event)
 {
-        bool rounded = QSettings().value("user/avatar/circles", true).toBool();
+        bool rounded = QSettings().value("user/avatar_circles", true).toBool();
 
         Q_UNUSED(event);
 
@@ -210,33 +210,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
                         p.setFont(QFont{});
                         p.setPen(subtitlePen);
 
-                        // The limit is the space between the end of the avatar and the start of the
-                        // timestamp.
-                        int usernameLimit =
-                          std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20);
-                        auto userName =
-                          metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit);
-
-                        p.setFont(QFont{});
-                        p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName);
-
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-                        int nameWidth = QFontMetrics(QFont{}).width(userName);
-#else
-                        int nameWidth = QFontMetrics(QFont{}).horizontalAdvance(userName);
-#endif
-                        p.setFont(QFont{});
-
-                        // The limit is the space between the end of the username and the start of
-                        // the timestamp.
-                        int descriptionLimit =
-                          std::max(0,
-                                   width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize -
-                                     nameWidth - 5);
+                        int descriptionLimit = std::max(
+                          0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize);
                         auto description =
                           metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit);
-                        p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y),
-                                   description);
+                        p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description);
 
                         // We show the last message timestamp.
                         p.save();
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/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 9fd033e9a0bdbf8aa7633466da626e50a7d77fce..1caea4494ebbe16683f62b2abaff2b6f77212a7f 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -53,7 +53,7 @@ UserSettings::load()
         isReadReceiptsEnabled_        = settings.value("user/read_receipts", true).toBool();
         theme_                        = settings.value("user/theme", defaultTheme_).toString();
         font_                         = settings.value("user/font_family", "default").toString();
-        avatarCircles_                = settings.value("user/avatar/circles", true).toBool();
+        avatarCircles_                = settings.value("user/avatar_circles", true).toBool();
         emojiFont_    = settings.value("user/emoji_font_family", "default").toString();
         baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
 
@@ -119,9 +119,7 @@ UserSettings::save()
         settings.setValue("start_in_tray", isStartInTrayEnabled_);
         settings.endGroup();
 
-        settings.beginGroup("avatar");
-        settings.setValue("circles", avatarCircles_);
-        settings.endGroup();
+        settings.setValue("avatar_circles", avatarCircles_);
 
         settings.setValue("font_size", baseFontSize_);
         settings.setValue("typing_notifications", isTypingNotificationsEnabled_);
diff --git a/src/Utils.cpp b/src/Utils.cpp
index c60adb58a1367d2b6a65c34176096379e73f0543..3e59d9129152b430670834d073fb836d6248cf82 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -40,9 +40,8 @@ utils::replaceEmoji(const QString &body)
         for (auto &code : utf32_string) {
                 // TODO: Be more precise here.
                 if (code > 9000)
-                        fmtBody +=
-                          QString("<span style=\"font-family: " + userFontFamily + ";\">") +
-                          QString::fromUcs4(&code, 1) + "</span>";
+                        fmtBody += QString("<font face=\"" + userFontFamily + "\">") +
+                                   QString::fromUcs4(&code, 1) + "</font>";
                 else
                         fmtBody += QString::fromUcs4(&code, 1);
         }
@@ -147,11 +146,6 @@ utils::getMessageDescription(const TimelineEvent &event,
                 const auto ts       = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
 
                 DescInfo info;
-                if (sender == localUser)
-                        info.username = QCoreApplication::translate("utils", "You");
-                else
-                        info.username = username;
-
                 info.userid    = sender;
                 info.body      = QString(" %1").arg(messageDescription<Encrypted>());
                 info.timestamp = utils::descriptiveTime(ts);
@@ -324,19 +318,29 @@ utils::linkifyMessage(const QString &body)
         return doc;
 }
 
-QByteArray escapeRawHtml(const QByteArray &data) {
-      QByteArray buffer;
-      const size_t length = data.size();
-      buffer.reserve(length);
-      for(size_t pos = 0; pos != length; ++pos) {
-            switch(data.at(pos)) {
-                  case '&':  buffer.append("&amp;");      break;
-                  case '<':  buffer.append("&lt;");       break;
-                  case '>':  buffer.append("&gt;");       break;
-                  default:   buffer.append(data.at(pos)); break;
-            }
-      }
-     return buffer;
+QByteArray
+escapeRawHtml(const QByteArray &data)
+{
+        QByteArray buffer;
+        const size_t length = data.size();
+        buffer.reserve(length);
+        for (size_t pos = 0; pos != length; ++pos) {
+                switch (data.at(pos)) {
+                case '&':
+                        buffer.append("&amp;");
+                        break;
+                case '<':
+                        buffer.append("&lt;");
+                        break;
+                case '>':
+                        buffer.append("&gt;");
+                        break;
+                default:
+                        buffer.append(data.at(pos));
+                        break;
+                }
+        }
+        return buffer;
 }
 
 QString
@@ -362,7 +366,7 @@ utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
 {
         return QString("<mx-reply><blockquote><a "
                        "href=\"https://matrix.to/#/%1/%2\">In reply "
-                       "to</a>* <a href=\"https://matrix.to/#/%3\">%4</a><br "
+                       "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br"
                        "/>%5</blockquote></mx-reply>")
                  .arg(related.room,
                       QString::fromStdString(related.related_event),
@@ -378,9 +382,6 @@ utils::getQuoteBody(const RelatedInfo &related)
         using MsgType = mtx::events::MessageType;
 
         switch (related.type) {
-        case MsgType::Text: {
-                return markdownToHtml(related.quoted_body);
-        }
         case MsgType::File: {
                 return QString(QCoreApplication::translate("utils", "sent a file."));
         }
diff --git a/src/Utils.h b/src/Utils.h
index 225754be4a5eb37ec0fbc2db261ee0cdc00d7fc7..bdb5184438fb82e39ab967ef0f5c3c4c97316c38 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -4,10 +4,6 @@
 
 #include "Cache.h"
 #include "RoomInfoListItem.h"
-#include "timeline/widgets/AudioItem.h"
-#include "timeline/widgets/FileItem.h"
-#include "timeline/widgets/ImageItem.h"
-#include "timeline/widgets/VideoItem.h"
 
 #include <QCoreApplication>
 #include <QDateTime>
@@ -94,38 +90,72 @@ messageDescription(const QString &username = "",
         using Video     = mtx::events::RoomEvent<mtx::events::msg::Video>;
         using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
 
-        // Sometimes the verb form of sent changes in some languages depending on the actor.
-        auto remoteSent = QCoreApplication::translate(
-          "message-description: ", "sent", "For when you are the sender");
-        auto localSent = QCoreApplication::translate(
-          "message-description:", "sent", "For when someone else is the sender");
-        QString sentVerb = isLocal ? localSent : remoteSent;
-        if (std::is_same<T, AudioItem>::value || std::is_same<T, Audio>::value) {
-                return QCoreApplication::translate("message-description sent:", "%1 an audio clip")
-                  .arg(sentVerb);
-        } else if (std::is_same<T, ImageItem>::value || std::is_same<T, Image>::value) {
-                return QCoreApplication::translate("message-description sent:", "%1 an image")
-                  .arg(sentVerb);
-        } else if (std::is_same<T, FileItem>::value || std::is_same<T, File>::value) {
-                return QCoreApplication::translate("message-description sent:", "%1 a file")
-                  .arg(sentVerb);
-        } else if (std::is_same<T, VideoItem>::value || std::is_same<T, Video>::value) {
-                return QCoreApplication::translate("message-description sent:", "%1 a video clip")
-                  .arg(sentVerb);
-        } else if (std::is_same<T, StickerItem>::value || std::is_same<T, Sticker>::value) {
-                return QCoreApplication::translate("message-description sent:", "%1 a sticker")
-                  .arg(sentVerb);
+        if (std::is_same<T, Audio>::value) {
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "You sent an audio clip");
+                else
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "%1 sent an audio clip")
+                          .arg(username);
+        } else if (std::is_same<T, Image>::value) {
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "You sent an image");
+                else
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "%1 sent an image")
+                          .arg(username);
+        } else if (std::is_same<T, File>::value) {
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "You sent a file");
+                else
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "%1 sent a file")
+                          .arg(username);
+        } else if (std::is_same<T, Video>::value) {
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "You sent a video");
+                else
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "%1 sent a video")
+                          .arg(username);
+        } else if (std::is_same<T, Sticker>::value) {
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "You sent a sticker");
+                else
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "%1 sent a sticker")
+                          .arg(username);
         } else if (std::is_same<T, Notice>::value) {
-                return QCoreApplication::translate("message-description sent:", "%1 a notification")
-                  .arg(sentVerb);
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "You sent a notification");
+                else
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "%1 sent a notification")
+                          .arg(username);
         } else if (std::is_same<T, Text>::value) {
-                return QString(": %1").arg(body);
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:", "You: %1")
+                          .arg(body);
+                else
+                        return QCoreApplication::translate("message-description sent:", "%1: %2")
+                          .arg(username)
+                          .arg(body);
         } else if (std::is_same<T, Emote>::value) {
                 return QString("* %1 %2").arg(username).arg(body);
         } else if (std::is_same<T, Encrypted>::value) {
-                return QCoreApplication::translate("message-description sent:",
-                                                   "%1 an encrypted message")
-                  .arg(sentVerb);
+                if (isLocal)
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "You sent an encrypted message");
+                else
+                        return QCoreApplication::translate("message-description sent:",
+                                                           "%1 sent an encrypted message")
+                          .arg(username);
         } else {
                 return QCoreApplication::translate("utils", "Unknown Message Type");
         }
@@ -135,29 +165,19 @@ template<class T, class Event>
 DescInfo
 createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id)
 {
-        using Text  = mtx::events::RoomEvent<mtx::events::msg::Text>;
-        using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>;
-
         const auto msg    = boost::get<T>(event);
         const auto sender = QString::fromStdString(msg.sender);
 
         const auto username = Cache::displayName(room_id, sender);
         const auto ts       = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
 
-        bool isText  = std::is_same<T, Text>::value;
-        bool isEmote = std::is_same<T, Emote>::value;
-
-        return DescInfo{
-          QString::fromStdString(msg.event_id),
-          isEmote ? ""
-                  : (sender == localUser ? QCoreApplication::translate("utils", "You") : username),
-          sender,
-          (isText || isEmote)
-            ? messageDescription<T>(
-                username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser)
-            : QString(" %1").arg(messageDescription<T>()),
-          utils::descriptiveTime(ts),
-          ts};
+        return DescInfo{QString::fromStdString(msg.event_id),
+                        sender,
+                        messageDescription<T>(username,
+                                              QString::fromStdString(msg.content.body).trimmed(),
+                                              sender == localUser),
+                        utils::descriptiveTime(ts),
+                        ts};
 }
 
 //! Scale down an image to fit to the given width & height limitations.
diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index dd9cd03ab8d5367fded1e374979e43c20d8f4c95..cbdd351cb28d435a0d5145b65496c719f85823a9 100644
--- a/src/dialogs/ImageOverlay.cpp
+++ b/src/dialogs/ImageOverlay.cpp
@@ -41,7 +41,6 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
         setAttribute(Qt::WA_DeleteOnClose, true);
         setWindowState(Qt::WindowFullScreen);
 
-        // Deprecated in 5.13: screen_ = QApplication::desktop()->availableGeometry();
         screen_ = QGuiApplication::primaryScreen()->availableGeometry();
 
         move(QApplication::desktop()->mapToGlobal(screen_.topLeft()));
diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp
index 9e973efa5d715b032e2aa068b41f1faea990c5a6..f62cf9fe4ae6385e7cf2385b8f11b79eb9dfbfc0 100644
--- a/src/dialogs/MemberList.cpp
+++ b/src/dialogs/MemberList.cpp
@@ -1,4 +1,5 @@
 #include <QAbstractSlider>
+#include <QLabel>
 #include <QListWidgetItem>
 #include <QPainter>
 #include <QPushButton>
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
index 00b034ccd71f747d34d41b3efd6c3e03cf34719f..25909cd86846f59d42b7fed55f5eaba669533665 100644
--- a/src/dialogs/RoomSettings.cpp
+++ b/src/dialogs/RoomSettings.cpp
@@ -488,7 +488,7 @@ RoomSettings::retrieveRoomInfo()
                 usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString());
                 info_           = cache::client()->singleRoomInfo(room_id_.toStdString());
                 setAvatar();
-        } catch (const lmdb::error &e) {
+        } catch (const lmdb::error &) {
                 nhlog::db()->warn("failed to retrieve room info from cache: {}",
                                   room_id_.toStdString());
         }
diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp
index 3480959ac79bd35d459f963c6aca195ebd2037e7..3be5c462ca356012afaf86d445162523e6565039 100644
--- a/src/popups/UserMentions.cpp
+++ b/src/popups/UserMentions.cpp
@@ -7,7 +7,7 @@
 #include "ChatPage.h"
 #include "Logging.h"
 #include "UserMentions.h"
-#include "timeline/TimelineItem.h"
+//#include "timeline/TimelineItem.h"
 
 using namespace popups;
 
@@ -116,39 +116,46 @@ UserMentions::pushItem(const QString &event_id,
                        const QString &room_id,
                        const QString &current_room_id)
 {
-        setUpdatesEnabled(false);
-
-        // Add to the 'all' section
-        TimelineItem *view_item = new TimelineItem(
-          mtx::events::MessageType::Text, user_id, body, true, room_id, all_scroll_widget_);
-        view_item->setEventId(event_id);
-        view_item->hide();
-
-        all_scroll_layout_->addWidget(view_item);
-        QTimer::singleShot(0, this, [view_item, this]() {
-                view_item->show();
-                view_item->adjustSize();
-                setUpdatesEnabled(true);
-        });
-
-        // if it matches the current room... add it to the current room as well.
-        if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
-                // Add to the 'local' section
-                TimelineItem *local_view_item = new TimelineItem(mtx::events::MessageType::Text,
-                                                                 user_id,
-                                                                 body,
-                                                                 true,
-                                                                 room_id,
-                                                                 local_scroll_widget_);
-                local_view_item->setEventId(event_id);
-                local_view_item->hide();
-                local_scroll_layout_->addWidget(local_view_item);
-
-                QTimer::singleShot(0, this, [local_view_item]() {
-                        local_view_item->show();
-                        local_view_item->adjustSize();
-                });
-        }
+        (void)event_id;
+        (void)user_id;
+        (void)body;
+        (void)room_id;
+        (void)current_room_id;
+        //        setUpdatesEnabled(false);
+        //
+        //        // Add to the 'all' section
+        //        TimelineItem *view_item = new TimelineItem(
+        //          mtx::events::MessageType::Text, user_id, body, true, room_id,
+        //          all_scroll_widget_);
+        //        view_item->setEventId(event_id);
+        //        view_item->hide();
+        //
+        //        all_scroll_layout_->addWidget(view_item);
+        //        QTimer::singleShot(0, this, [view_item, this]() {
+        //                view_item->show();
+        //                view_item->adjustSize();
+        //                setUpdatesEnabled(true);
+        //        });
+        //
+        //        // if it matches the current room... add it to the current room as well.
+        //        if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
+        //                // Add to the 'local' section
+        //                TimelineItem *local_view_item = new
+        //                TimelineItem(mtx::events::MessageType::Text,
+        //                                                                 user_id,
+        //                                                                 body,
+        //                                                                 true,
+        //                                                                 room_id,
+        //                                                                 local_scroll_widget_);
+        //                local_view_item->setEventId(event_id);
+        //                local_view_item->hide();
+        //                local_scroll_layout_->addWidget(local_view_item);
+        //
+        //                QTimer::singleShot(0, this, [local_view_item]() {
+        //                        local_view_item->show();
+        //                        local_view_item->adjustSize();
+        //                });
+        //        }
 }
 
 void
@@ -158,4 +165,4 @@ UserMentions::paintEvent(QPaintEvent *)
         opt.init(this);
         QPainter p(this);
         style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
\ No newline at end of file
+}
diff --git a/src/timeline/.TimelineItem.cpp.swp b/src/timeline/.TimelineItem.cpp.swp
deleted file mode 100644
index 75e03aebe8a2b20a05aec339ab9cab819ed7fe2e..0000000000000000000000000000000000000000
--- a/src/timeline/.TimelineItem.cpp.swp
+++ /dev/null
@@ -1,186 +0,0 @@
-b0VIM 8.1������€c]7B[�'"��nicolas���������������������������������gentoo-neko�����������������������������~nicolas/Dokumente/devel/open-source/nheko/src/timeline/TimelineItem.cpp�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������utf-8
�3210����#"! U�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������tp�����������/����������������������������0�������1����������������������������a����������������������������x���������������������@�������~����������������������������¿��������������	��������������Ó����������������������������à���������������������;�������ö���������������������B�������0��������������������-�������s���������������������������Ÿ���������������������������¿��������������������I�������Á��������������������3�������	��������������������;�������=�������������
�������
-�������w��������������������<�������‚���������������������������¾���������������������������Ì���������������������������ß��������������������K�������ë���������������������������6�������������
-�������4�������M��������������������9�������‚���������������������������º�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ad��		��á	�����/�������ý��¸��µ��m��%��à��¹��¶��s��1��ð
����½
��x
��/
��+
��
��
��÷��Û��Â��±��Ÿ��‹��Š��t��`��K��3��"����ó��Ù��Ø��µ����f��>������õ
-��Ö
-��Õ
-��
-��d
-��c
-��á	��
-��C
-��
-��Ñ	��	��;	��9	��8	��3	��þ��ü��Ð��Ï��¬��d��*��)��à��Þ��Ý��Ø��­��«��x��`��_��F������þ��ý��ã��¸��Ž��w��m��?����ÿ��Ð��¢��‹����V��"������×��À��¶��´��³��®��x��v��^��]��D����é��Ò��¥��x��a��8����ù��Ð��¨��‘��g��G��0��&��%�������������}�        update();��        }�                break;�                setToolTip("");�        case StatusIndicatorState::Empty:�                break;�                setToolTip(tr("Sent"));�        case StatusIndicatorState::Sent:�                break;�                setToolTip(tr("Seen"));�        case StatusIndicatorState::Read:�                break;�                setToolTip(tr("Delivered"));�        case StatusIndicatorState::Received:�                break;�                setToolTip(tr("Encrypted"));�        case StatusIndicatorState::Encrypted:�        switch (state) {��        state_ = state;�{�StatusIndicator::setState(StatusIndicatorState state)�void��}�        }�                break;�        case StatusIndicatorState::Empty:�        }�                break;�                paintIcon(p, doubleCheckmarkIcon_);�        case StatusIndicatorState::Read: {�        }�                break;�                paintIcon(p, checkmarkIcon_);�        case StatusIndicatorState::Received: {�                break;�                paintIcon(p, lockIcon_);�        case StatusIndicatorState::Encrypted:�        }�                break;�                paintIcon(p, clockIcon_);�        case StatusIndicatorState::Sent: {�        switch (state_) {��        p.setPen(iconColor_);��        PainterHighQualityEnabler hq(p);�        Painter p(this);��                return;�        if (state_ == StatusIndicatorState::Empty)�{�StatusIndicator::paintEvent(QPaintEvent *)�void��}�        QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal);��        painter.fillRect(pixmap.rect(), p.pen().color());�        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);�        QPainter painter(&pixmap);��        auto pixmap = icon.pixmap(width());�{�StatusIndicator::paintIcon(QPainter &p, QIcon &icon)�void��}�        doubleCheckmarkIcon_.addFile(":/icons/StatusIndicator::StaStatusIndicator::StaStatusIndicator::StatusIndicator(QWidget *parent)�������úw.�������������?¬StatusIndicator::StatusIndicator(QWidget *parent)�������úw.�������������úw.	�������"������úw.�������+������úw.��������constexpr int MSG_PADDING      = 20;�������úw.�������constexpr int MSG_RIGHT_MARGIN = 7;�������úw.��������#include "mtx/identifiers.hpp"�#include "dialogs/RawMessage.h"��#include "timeline/widgets/VideoItem.h"�#include "timeline/widgets/ImageItem.h"�#include "timeline/widgets/FileItem.h"�#include "timeline/widgets/AudioItem.h"�#include "timeline/TimelineItem.h"��#include "ui/TextLabel.h"�#include "ui/Painter.h"�#include "ui/Avatar.h"�#include "Olm.h"�#include "MainWindow.h"�#include "Logging.h"�#include "Config.h"�#include "ChatPage.h"��#include <QtGlobal>�#include <QTimer>�#include <QMenu>�#include <QFontDatabase>�#include <QDesktopServices>�#include <QContextMenuEvent>��#include <functional>� */� * along with this program.  If not, see <http://www.gnu.org/licenses/>.� * You should have received a copy of the GNU General Public License� *� * GNU General Public License for more details.� * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the� * but WITHOUT ANY WARRANTY; without even the implied warranty of� * This program is distributed in the hope that it will be useful,� *� * (at your option) any later version.� * the Free Software Foundation, either version 3 of the License, or� * it under the terms of the GNU General Public License as published by� * This program is free software: you can redistribute it and/or modify� *� * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>�/*�ad��x��°������������™��X��*��Ô��À��²��°��Ã��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������}�          });�    }�          });�                  }�                            "failed to serialize event ({}, {})", room_id, event_id);�                          nhlog::net()->warn(�                  } catch (const nlohmann::json::exception &e) {�                          emit proxy->eventRetrieved(utils::serialize_event(res));�������úw.
�������ad��ç��/�����K�������Þ����†��8����Ú��Ù��H����ã
��â
��œ
��o
��I
��"
��
��
��Î��¡��z��S��G��F��÷��«��Ÿ����œ��—������ç
-��º
-��
-��B
-��@
-��?
-��:
-��±	��¯	��‡	��3	��2	��ù��Ð��Ï��‰��K��J��	��Ò��Œ��‹��Y������ô����›��š��•��K��I����ç��æ��Ÿ��T����å��º��¸��·��²��/��Ô��������Ó��Ñ��Ð��Ë��B��@��&��î��ì��ë��æ��‚��€��R��:��ø��µ��³��²��­�����������void��}�        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);�        QPainter p(this);�	������?¬…±�������������?¬…±�������        opt.init(this);�        QStyleOptTimelineItem::setUseTimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QStrinTimelineItem::setUserAvatar(const QStrinTimelineItem::setUserAvatar(const QStrinTimelineItem::setUserAvatar(const QString &userid)�������úw.�������������?TimelineItem::setUserAvatar(const QString &userid)�������úw.����������
���úw.�������#������úw.�������,������úw.�������void��}�                                       0);�                                       0,�                                       conf::timeline::msgTopMargin,�                                         QFontMetrics(f).height() * 2 + 2,�        topLayout_->setContentsMargins(conf::timeline::msgLeftMargin +��        f.setPointSizeF(f.pointSizeF());�        QFont f;�	������úw.�������������úw.�������{�TimelineItem::setupSimpleLayout()�������úw.�������������úw.�������void��}�                mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft);�        if (userName_)��        topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft);�        topLayout_->insertWidget(0, userAvatar_);��                userAvatar_->setLetter(QChar(userName[1]).toUpper());�        if (userName[0] == '@' && userName.size() > 1)�        // TODO: The provided user name should be a UserId class��        userAvatar_->setLetter(QChar(userName[0]).toUpper());�        userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2);��        f.setPointSizeF(f.pointSizeF());�        QFont f;�	������úw.�������������úw.��������          conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0);�        topLayout_->setContentsMargins(�{�TimelineItem::setupAvatarLayout(const QString &userName)�������úw.�������������úw.�������'������úw.�������0������úw.�������void��}�          QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));�        timestamp_->setText(�        timestamp_->setFont(timestampFont_);�        timestamp_ = new QLabel(this);�{�TimelineItem::generateTimestamp(const QDateTime &time)�������úw.�������������úw.�������'���	���úw.�������2������úw.�������void��}�        });�                MainWindow::instance()->openUserProfile(user_id, room_id_);�        connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() {��        });�                userName_->setFont(f);�                f.setUnderline(false);�                QFont f = userName_->font();�        connect(filter, &UserProfileFilter::hoverOff, this, [this]() {��        });�                userName_->setFont(f);�                f.setUnderline(true);�                QFont f = userName_->font();�        connect(filter, &UserProfileFilter::hoverOn, this, [this]() {��        userName_->setCursor(Qt::PointingHandCursor);�        userName_->installEventFilter(filter);�        auto filter = new UserProfileFilter(user_id, userName_);�������úw.�������������úw.�������-������úw.�������6���	���úw.
-��������        refreshAuthorColor();�	������úw.�������        // otherwise this will just set it.�        // Set the user color asynchronously if it hasn't been generated yet,�#endif�          QFontMetrics(userName_->font()).horizontalAdvance(userName_->text()));�        userName_->setFixedWidth(�ad��õ��=
������������Ú��°��S��+��þ��Ï����<��ï
��k
��=
��›
��B
��;
��í��Á��£��¢��a��2��ü��û��µ��ˆ��b��;��/��.��ç
-��º
-��“
-��l
-��`
-��_
-��
-��Ä	��¸	��¶	��µ	��°	��y	��w	��P	��#	��	��«��©��¨��£��j��h��@��ì��ë��Ú��±��°��j��,��+��ê��³��m��l��:��í��ì��Õ��~��|��{��v��T��R��A������Ð��…��@����ë��é��è��ã��°��®��Š��r��q��@��>��=��8��ÿ��ý��ã��«��©��¨��£��{��y��_��G��-��ê��è��ç��â��	����������������void��}�        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);�        QPainter p(this);�        opt.init(this);�        QStyleOption opt;�{�TimelineItem::paintEvent(QPaintEvent *)�void��}�                contextMenu_->exec(event->globalPos());�        if (contextMenu_)�{�TimelineItem::contextMenuEvent(QContextMenuEvent *event)�void��}�        userAvatar_->setImage(room_id_, userid);��                return;�        if (userAvatar_ == nullptr)�{�TimelineItem::setUserAvatar(const QString &userid)�void��}�                                       0);�                                       0,�                                       conf::timeline::msgTopMargin,�                                         QFontMetrics(f).height() * 2 + 2,�        topLayout_->setContentsMargins(conf::timeline::msgLeftMargin +��        f.setPointSizeF(f.pointSizeF());�        QFont f;�{�TimelineItem::setupSimpleLayout()�void��}�                mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft);�        if (userName_)��        topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft);�        topLayout_->insertWidget(0, userAvatar_);��                userAvatar_->setLetter(QChar(userName[1]).toUpper());�        if (userName[0] == '@' && userName.size() > 1)�        // TODO: The provided user name should be a UserId class��        userAvatar_->setLetter(QChar(userName[0]).toUpper());�        userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2);��        f.setPointSizeF(f.pointSizeF());�        QFont f;��          conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0);�        topLayout_->setContentsMargins(�{�TimelineItem::setupAvatarLayout(const QString &userName)�void��}�          QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));�        timestamp_->setText(�        timestamp_->setFont(timestampFont_);�        timestamp_ = new QLabel(this);�{�TimelineItem::generateTimestamp(const QDateTime &time)�void��}�        });�                MainWindow::instance()->openUserProfile(user_id, room_id_);�        connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() {��        });�                userName_->setFont(f);�                f.setUnderline(false);�                QFont f = userName_->font();�        connect(filter, &UserProfileFilter::hoverOff, this, [this]() {��        });�                userName_->setFont(f);�                f.setUnderline(true);�                QFont f = userName_->font();�        connect(filter, &UserProfileFilter::hoverOn, this, [this]() {��        userName_->setCursor(Qt::PointingHandCursor);�        userName_->installEventFilter(filter);�        auto filter = new UserProfileFilter(user_id, userName_);��        refreshAuthorColor();�        // otherwise this will just set it.�        // Set the user color asynchrono#else��������ZhÑø�������������oÁ€�#else��������ZhÑø�������������oÁ€������        userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text()));����\���ZhÑø���������\���oÁ€������        // width deprecated in 5.13:����%���ZhÑø���������%���oÁ€������#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)����
-���úw.
�������������úw.
�������        userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop);�        userName_->setAttribute(Qt::WA_Hover);�        userName_->setToolTipDuration(1500);�        userName_->setToolTip(user_id);�        userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500)));�        userName_->setFont(usernameFont);�        userName_ = new QLabel(this);�ad��Ù��©�����-�������À��e����ý��Ê��´��²��Ž����G��E��D��Ç
��m
��-
��Ò��~��j��7��!����û��ú��´��²��±��4��Ç
-��†
-��+
-��×	��Ã	��	��z	��x	��!	��û��ú��É��È��}��{��z��û��©���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������                                               const mtx::ev                           const mtx::ev                                               const mtx::events::Sticker &event,�8������úw.�����                           const mtx::ev                                               const mtx::ev                           const mtx::ev                                               const mtx::events::Sticker &event,�8������úw.�����                                                                   const mtx::ev                           const mtx::ev                           const mtx::ev                           const mtx::events::Sticker &event,�8������úw.�����                                               const mtx::events::Sticker &event,�8������úw.�������TimelineItem::TimelineItem(StickerItem *image,�������úw.�������������úw.	�������������úw.�������)������úw.��������}�        addSaveImageAction(image);�	������úw.�������������úw.��������        markOwnMessagesAsReceived(event.sender);��          image, event, with_sender);�        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(�{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Image)�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool with_sender,�!������úw.�������                           const mtx::events::RoomEvent<mtx::events::msg::Image> &event,�S������úw.�������TimelineItem::TimelineItem(ImageItem *image,�������úw.�������������úw.	����������	���úw.�������'������úw.��������}�        setupLocalWidgetLayout<VideoItem>(video, userid, withSender);��        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Video)�  : QWidget{parent}�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool withSender,�!���
-���úw.�������                           const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(VideoItem *video,�������úw.�������������úw.	����������	���úw.�������'������úw.��������}�        setupLocalWidgetLayout<AudioItem>(audio, userid, withSender);��        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Audio)�  : QWidget{parent}�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool withSender,�!���
-���úw.�������ad��Q��u�����B�������²����Z��/��Ý��—��h��W��&��ú
��Ï
��¦
��`
��1
��'
��&
��æ��³��²��d��c��5��ò��Â��Á��r��a��3��ú
-��ð
-��ï
-��¼
-��º
-��¹
-��<
-��â	��¢	��G	��ó��ß��¬��–��”��p��o��)��(��Ý��Û��Ú��_����Å��j������Ð��º��¸��”��“��O��M��L��Ï��u��;��"��ö��â��¯��™��—��‡��†��@��>��=����Þ��²����S��?��                           const QString                                               const QString                                                                   const QString &userid,�"������úw.�������"������?¬                           const QString                                               const QString                           const QString                                               const QString                           const QString &userid,�"������úw.�������"������?¬                           const QString                                               const QString                           const QString                                               const QString &userid,�"������úw.�������"������?¬                           const QString                           const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(AudioItem *audio,�������úw.�������������úw.	����������	���úw.�������'������úw.��������}�        setupLocalWidgetLayout<FileItem>(file, userid, withSender);��        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::File)�  : QWidget{parent}�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool withSender,�!���
-���úw.�������                           const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(FileItem *file,�������úw.�������������úw.	�������������úw.�������&������úw.��������}�        addSaveImageAction(image);�	������úw.�������������úw.��������        setupLocalWidgetLayout<ImageItem>(image, userid, withSender);��        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Image)�  : QWidget{parent}�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool withSender,�!���
-���úw.�������                           const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(ImageItem *image,�������úw.�������������úw.	����������	���úw.�������'������úw.��������}�        adjustMessageLayout();�	������úw.��������        }�                setupSimpleLayout();�������úw.�������                generateBody(formatted_body);�        } else {�                setUserAvatar(userid);����
���úw.�������������úw.��������                setupAvatarLayout(displayName);�                generateBody(userid, displayName, formatted_body);�        if (withSender) {�
���
-���úw.��������        generateTimestamp(timestamp);�	������úw.����������	���úw.��������        formatted_body.replace("mx-reply", "div");�        formatted_body = utils::linkifyMessage(formatted_body);��        }�                                   timestamp};�                                   utils::descriptiveTime(timestamp),�                                   body,�                                   userid,�                                   "You: ",�                descriptionMsg_ = {emptyEventId,�        } else {�                                   timestamp};�                                   utils::descriptiveTime(timestamp),�                                   QString("* %1 %2").arg(displayName).arg(body),�                                   userid,�                                   "",�                descriptionMsg_ = {emptyEventId,�                formatted_body  = QString("<em>%1</em>").arg(formatted_body);�ad��‹��ÿ������������®��\����ä��ã��ª��„��ƒ��[��
��ê
��¾
��½
��i
��E
��D
��
��î��³��B��A��ÿ��å��À��w��#��"��ö
-��Æ
-��‰
-��
-��
-��å	��	��q	��&	��ú��ù��°����J��I��ö��õ��¿��v��,��+��Ö��‘����Ž��‹��j��f��ò��˜��G����¬��l��X��B��,��*����Ø��×��‚��
����Ú��‹��Š��T����ß��Þ��˜��—��b��������        if (ty == mtx::events::MessageType::Emote) {��        QString emptyEventId;�	������?¬…±�������������?¬…±��������                formatted_body = body.toHtmlEscaped();�        if (formatted_body == body.trimmed().toHtmlEscaped())�        // Escape html if the input is not formatted.��        auto formatted_body = utils::markdownToHtml(body);�������?¬…±�������        // Generate the html body to be rendered.��        auto timestamp   = QDateTime::currentDateTime();����	���?¬…±����������	���?¬…±�������'������?¬…±�������        auto displayName = Cache::displayName(room_id_, userid);�������?¬…±��������        addReplyAction();�	������?¬…±�������        init();�	������?¬…±�������{�  , room_id_{room_id}�  , message_type_(ty)�  : QWidget(parent)�                           QWidget *parent)�%������?¬…±�������                           const QString &room_id,�"������?¬…±�������+������?¬…±�������                           bool withSender,�!���
-���?¬…±�������                           QString body,�������?¬…±�������$������?¬…±�������                           const QString &userid,�"������?¬…±�������+������?¬…±�������TimelineItem::TimelineItem(mtx::events::MessageType ty,�������?¬…±�������������?¬…±	�������5������?¬…±������� */� * For messages created locally.�/*��}�        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);�        parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);��        statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());�        statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());�        statusIndicator_ = new StatusIndicator(this);��        QFontMetrics tsFm(timestampFont_);�	������?¬…±�������������?¬…±��������        timestampFont_.setStyleHint(QFont::Monospace);�        timestampFont_.setFamily("Monospace");�        timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);��        contextBtn_->setMenu(contextMenu_);�        contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));�        contextBtn_->setIcon(context_icon);�        context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");�        QIcon context_icon;�	������?¬…±��������        contextBtn_->setCornerRadius(buttonSize_ / 2);�	������?¬…±
-�������������?¬…±�������&������?¬…±�������        contextBtn_->setFixedSize(buttonSize_, buttonSize_);�        contextBtn_->setToolTip(tr("Options"));�        contextBtn_ = new FlatButton(this);��        connect(replyBtn_, &FlatButton::        QIcon reply_icon;�	������úw.�        QIcon reply_icon;�	������úw.�        QIcon reply_icon;�	������úw.�������	������?¬…±�        QIcon reply_icon;�	������úw.�        QIcon reply_icon;�	������úw.����������
-���úw.��������        replyBtn_->setCornerRadius(buttonSize_ / 2);�	���	���úw.
-�������������úw.�������$������úw.�������        replyBtn_->setFixedSize(buttonSize_, buttonSize_);�        replyBtn_->setToolTip(tr("Reply"));�        replyBtn_ = new FlatButton(this);��        mainLayout_->setSpacing(0);�        mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);��        topLayout_->addLayout(mainLayout_);�        topLayout_->setSpacing(0);�          conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0);�        topLayout_->setContentsMargins(��        actionLayout_->setSpacing(0);�        actionLayout_->setContentsMargins(13, 1, 13, 0);��        messageLayout_->setSpacing(MSG_PADDING);�        messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0);�        actionLayout_  = new QHBoxLayout;�	���
���úw.
-�������������úw.�������        messageLayout_ = new QHBoxLayout;�	������úw.
-�������������úw.�������ad��/
��{
��������������D��â��á��£����M��7��þ
��ý
��Í
��{
��e
��O
��
��×��Ö����w��v��N�����Ý��±��°��\��8��7��
��á
-��¦
-��q
-��p
-��V
-��
-��ï	��¦	��R	��Q	��%	��õ��¸����€��d����ð��¥��y��x��/�����É��È����œ��f����Ó��Ò��}��8��6��5��2����
��Õ��£��z��N����ï��Û��Å��¯��­����ƒ��‚��A������Õ��š��™��c��%��î��í��Ï��Î��™��˜�����������        if (ty == mtx::events::MessageType::Emote) {��        QString emptyEventId;��                formatted_body = body.toHtmlEscaped();�        if (formatted_body == body.trimmed().toHtmlEscaped())�        // Escape html if the input is not formatted.��        auto formatted_body = utils::markdownToHtml(body);�        // Generate the html body to be rendered.��        auto timestamp   = QDateTime::currentDateTime();�        auto displayName = Cache::displayName(room_id_, userid);��        addReplyAction();�        init();�{�  , room_id_{room_id}�  , message_type_(ty)�  : QWidget(parent)�                           QWidget *parent)�                           const QString &room_id,�                           bool withSender,�                           QString body,�                           const QString &userid,�TimelineItem::TimelineItem(mtx::events::MessageType ty,� */� * For messages created locally.�/*��}�        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);�        parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);��        statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());�        statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());�        statusIndicator_ = new StatusIndicator(this);��        QFontMetrics tsFm(timestampFont_);��        timestampFont_.setStyleHint(QFont::Monospace);�        timestampFont_.setFamily("Monospace");�        timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);��        contextBtn_->setMenu(contextMenu_);�        contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));�        contextBtn_->setIcon(context_icon);�        context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");�        QIcon context_icon;��        contextBtn_->setCornerRadius(buttonSize_ / 2);�        contextBtn_->setFixedSize(buttonSize_, buttonSize_);�        contextBtn_->setToolTip(tr("Options"));�        contextBtn_ = new FlatButton(this);��        connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction);�        replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));�        replyBtn_->setIcon(reply_icon);�        reply_icon.addFile(":/icons/icons/ui/mail-reply.png");�        QIcon reply_icon;��        replyBtn_->setCornerRadius(buttonSize_ / 2);�        replyBtn_->setFixedSize(buttonSize_, buttonSize_);�        replyBtn_->setToolTip(tr("Reply"));�        replyBtn_ = new FlatButton(this);��        mainLayout_->setSpacing(0);�        mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);��        topLayout_->addLayout(mainLayout_);�        topLayout_->setSpacing(0);�          conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0);�        topLayout_->setContentsMargins(��        actionLayout_->setSpacing(0);�        actionLayout_->setContentsMargins(13, 1, 13, 0);��        messageLayout_->setSpacing(MSG_PADDING);�        messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0);�            mainLayout_    = new QVBoxLayout        mainLayout_    = new QVBoxLayout;�	������úw.
-�������������úw.�������        topLayout_     = new QHBoxLayout(this);��                &TimelineItem::finishedGeneratingColor);�                this,�                &QFutureWatcher<QString>::finished,�        connect(colorGenerating_,�        colorGenerating_ = new QFutureWatcher<QString>(this);��        connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer);�        connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt);�          ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor);�ad��`��L�����4�������}��{��K��õ��Á��À��l��b��`��_��Z������á
��Ž
��X
��W
��ú��ð��î��í��è��¤��¢��z��b��a����í��·����F������}
-��{
-��z
-��u
-��)
-��'
-��÷	��’	��\	��[	��	��´�� ��–��”��“��Ž��L��
-��%
-��
-��
-��Ê	��—	��V	��U	��&	��%	��ý��ü��Ý��Û��Ú��Õ��¯��­��‹��H����µ��w��(��Ê��u����é��º��¸��·��²��‡��…��:������Î��n����ð��ä��ã��À��­��™��`����Ó��Ò��µ��‡��0����ú��ù��á���������������������                  try {��                  }�                          return;�                            "failed to retrieve event {} from {}", event_id, room_id);�                          nhlog::net()->warn(�                  if (err) {��                  using namespace mtx::events;�            const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) {�          [event_id, room_id, proxy = std::move(proxy)](�          event_id,�          room_id,�        http::client()->get_event(��        });�                Q_UNUSED(dialog);�                auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))};�        connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) {�        auto proxy = std::make_shared<EventProxy>();��        const auto room_id  = room_id_.toStdString();�        const auto event_id = event_id_.toStdString();�������?¬…±�������{�TimelineItem::openRawMessageViewer() const�void��}�                                           });�                                                   }�                                      TimelineItem::addAvatar()�������úw.�������������?¬…±����������	���úw.�TimelineItem::addAvaTimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvaTimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvatar()�������úw.�������������?¬…±����������	���úw.����������	���?¬…±�TimelineItem::addAvatar()�������úw.�TimelineItem::addAvaTimelineItem::addAvaTimelineItem::addAvatar()�������úw.�TimelineItem::addAvatar()�������úw.����������	���úw.�������void��}�        }�                });�                        olm::request_keys(room_id_.toStdString(), event_id_.toStdString());�                connect(requestKeys, &QAction::triggered, this, [this]() {��                contextMenu_->addAction(requestKeys);�                auto requestKeys = new QAction("Request encryption keys", this);�������úw.�������        if (contextMenu_) {�
������úw.
-�������{�TimelineItem::addKeyRequestAction()�������úw.�������������úw.�������void��}�        emit ChatPage::instance()->messageReply(related);�	������úw.
�������������úw.�������������úw.�������$������úw.�������1������úw.��������        related.room          = room_id_;�        related.related_event = eventId().toStdString();�        related.quoted_user   = descriptionMsg_.userid;�        related.quoted_body   = body_->toPlainText();�        related.type          = message_type_;�        RelatedInfo related;�	������úw.�������������úw.��������                return;�        if (!body_)�������úw.
-�������{�TimelineItem::replyAction()�������úw.�������������úw.�������void��}�        }�                connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction);��                contextMenu_->addAction(replyAction);�                auto replyAction = new QAction("Reply", this);�������úw.�������        if (contextMenu_) {�
������úw.
-�������{�TimelineItem::addReplyAction()�������úw.�������������úw.�������void��}�        }�                connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs);��                contextMenu_->addAction(saveImage);�                auto saveImage = new QAction("Save image", this);����	���úw.�������        if (contextMenu_) {�
������úw.
-�������{�TimelineItem::addSaveImageAction(ImageItem *image)�������úw.�������������úw.�������"���	���úw.�������-������úw.�������ad��‹	��#
-������������¿��d����ü��æ��ä��‹��Š��Y��X��5��3��2��·
��K
��
-
��¯��[��G����ÿ��ý��¨��ƒ��‚��Q��O��N��Ñ
-��d
-��#
-��r��T��@��
��÷
-��õ
-��ž
-��x
-��w
-��F
-��D
-��C
-��
-��½	��	��]	��1	��	��ê��Ô��Ò��{��U��T��#��!�� ����ò��î��”��g��4����ô��À��ª��¨��˜��~��}��L��K����¿��h��g��
-��¦��¥����>����è��ª��ƒ��‚��\��[��@��÷��ö��³��ƒ��‚��[��J����÷��í��ì��Í��Ë��Ê��Ç��Ú�����/*��}�        adjustMessageLayout();��        }�                setupSimpleLayout();�                generateBody(formatted_body);�        } else {�                setUserAvatar(sender);��                setupAvatarLayout(displayName);�                generateBody(sender, displayName, formatted_body);��                auto displayName = Cache::displayName(room_id_, sender);�        if (with_sender) {��        generateTimestamp(timestamp);��                           timestamp};�                           utils::descriptiveTime(timestamp),�                           " sent a notification",�                           sender,�                           Cache::displayName(room_id_, sender),�        descriptionMsg_ = {event_id_,��        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());��        const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);�        const auto sender    = QString::fromStdString(event.sender);�        event_id_            = QString::fromStdString(event.event_id);��        markOwnMessagesAsReceived(event.sender);��        addReplyAction();�        init();�{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Notice)�  : QWidget(parent)�                           QWidget *parent)�                           const QString &room_id,�                           bool with_sender,�TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,� */� * Used to display remote notice messages.�/*��}�        markOwnMessagesAsReceived(event.sender);��          video, event, with_sender);�                                                   bool with_sen                           bool with_sen                           bool with_sender,�!������úw.�������!������?¬…±��                                                                   bool with_sen                           bool with_sen                           bool with_sender,�!������úw.�������!������?¬…±��                                                                   bool with_sender,�!������úw.�������                           const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,�S������úw.�������TimelineItem::TimelineItem(AudioItem *audio,�������úw.�������������úw.	����������	���úw.�������'������úw.��������}�        markOwnMessagesAsReceived(event.sender);��          file, event, with_sender);�        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(�{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::File)�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool with_sender,�!������úw.�������                           const mtx::events::RoomEvent<mtx::events::msg::File> &event,�R������úw.�������TimelineItem::TimelineItem(FileItem *file,�������úw.�������������úw.	�������������úw.�������&������úw.��������}�        addSaveImageAction(image);��        markOwnMessagesAsReceived(event.sender);��        setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender);�{�  , room_id_{room_id}�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool with_sender,�!������úw.�������ad��;��§������������¼��º��¹��´��é��ç��°��¯��„��J����»
��±
��°
��l
��'
��ø��÷��¨��§����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������        QFontMetrics fm(usernameFont);��        QFontMetrics fm(usernameFont);�	������úw.�������	������?¬…±�������������úw.�������������?¬…±��������        QFontMetric�        QFontMetrics fm(usernameFont);��        QFontMetrics fm(usernameFont);��        QFontMetrics fm(usernameFont);�	������úw.�������������úw.��������        usernameFont.setWeight(QFont::Medium);�        usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1);�        QFont usernameFont;�	������úw.�������������úw.��������        }�                        sender = displayname.split(":")[0].split("@")[1];�                if (displayname.split(":")[0].split("@").size() > 1)�                // TODO: Fix this by using a UserId type.�        if (displayname.startsWith("@")) {��        auto sender = displayname;�������úw.�������{�TimelineItem::generateUserName(const QString &user_id, const QString &displayname)�������úw.�������������úw.�������&������úw.�������/������úw.�������>������úw.�������G������úw.�������void��}�        generateBody(body);�	������úw.�������������úw.�������ad��
��T
�����
-�������¼��m��Ñ��Ï��Î��É��ˆ��†��ì
��T
��Ì����˜��ó��ñ��­��¬��k��¦
-��™
-��Õ	��Ô	��¥	��£	��¢	��ƒ	��~	��Ö��Ô��”��4��3��×��–��8��,��*��)��$��Å��Ã��€��S��'��ò��è��§��v��7��ç��œ��r��_��^��Ö��Õ��‹��`����õ��š��ˆ��~��|��{��v������­��9��Ñ����¿        QString userColor = colorGenerating_->result();�	������?¬…±����������	���úw.����������	���?¬…±�������        nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString());�{�TimelineItem::finishedGeneratingColor()�������úw.�������������?¬…±�������������?¬…±�������void��}�        }�                }�                        userName_->setStyleSheet("QLabel { color : " + userColor + "; }");�                } else {�                        colorGenerating_->setFuture(QtConcurrent::run(generate));�                if (userColor.isEmpty()) {�                // If the color is empty, then generate it asynchronously��                QString userColor = Cache::userColor(userName_->toolTip());�������?¬…±����������	���úw.����������	���?¬…±��������                };�                        return userColor;�                          userName_->toolTip(), backgroundColor().name());�                        QString userColor = utils::generateContrastingHexColor(�                std::function<QString()> generate = [this]() {�                // generate user's unique color.�        if (userName_) {�
���	���úw.
-�������
���	���?¬…±
-�������        }�                colorGenerating_->waitForFinished();�                colorGenerating_->cancel();�        if (colorGenerating_->isRunning()) {�        // Cancel and wait if we are already generating the color.�{�TimelineItem::refreshAuthorColor()�������úw.�������������?¬…±�������������?¬…±�������void��}�        });�                                                        ChatPage::instance()->currentRoom());�                MainWindow::instance()->openUserProfile(user_id,�        connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {��        body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);�        body_ = new TextLabel(utils::replaceEmoji(body), this);�{�TimelineItem::generateBody(const QString &body)�������úw.�������������?¬…±�������������?¬…±�������"������?¬…±�������+������úw.�������+������?¬…±�������void�// Only the body is displayed.��}�        sendReadReceipt();�	������?¬…±��������                statusIndicator_->setState(StatusIndicatorState::Received);�������úw.
-�������������?¬…±
-�������#������?¬…±�������,������?¬…±�������B������úw.�������B������?¬…±�������        else�                statusIndicator_->setState(StatusIndicatorState::Encrypted);�������úw.
-�������������?¬…±
-�������#������?¬…±�������,������?¬…±�������B���	���úw.�������B���	���?¬…±�������        if (isEncrypted)�
������úw.�������
������?¬…±��������        isReceived_ = true;�	��                statusIndicator_->setState(StatusIndicatorState::Read);�                statusIndicator_->setSta                statusIndicator_->setState(StatusIndicatorState::Read);�������                statusIndicator_->setSta                statusIndicator_->setState(StatusIndicatorState::Read);�������                statusIndicator_->setState(StatusIndicatorState::Read);�������                statusIndicator_->setState(StatusIndicatorState::Read);�������úw.
-�������#������úw.�������,������úw.�������B������úw.�������        if (statusIndicator_->state() != StatusIndicatorState::Encrypted)�
������úw.
-�������������úw.�������*������úw.�������@���	���úw.�������{�TimelineItem::markRead()�������úw.�������������úw.�������void��}�                statusIndicator_->setState(StatusIndicatorState::Received);�������úw.
-�������#������úw.�������,������úw.�������B������úw.�������        if (sender == settings.value("auth/user_id").toString().toStdString())�        QSettings settings;�	���	���úw.�������������úw.�������ad��-��Q������������¥��Q��™��Î��¸��¶��_��9��8��������7
��¶��a��Þ��v��b��/������À
-��š
-��™
-��h
-��f
-��e
-��b
-��7
-��3
-��a	��	��‰��!��
��Ù��Ã��Á��‰��G��F������Í��`��á��à��[��Ï��Î��¨��g��D����Ó��¬��«��…��„��A��Ð��Ï��Œ��\��[��4��#��õ��¨��ž����V��T��S��P��������������/*��}�        adjustMessageLayout();�	������úw.�������	������?¬…±��������        }�                setupSimpleLayout();�������úw.�������������?¬…±�������                generateBody(formatted_body);�        } else {�                setUserAvatar(sender);��                setupAvatarLayout(displayName);�                generateBody(sender, displayName, formatted_body);��                auto displayName = Cache::displayName(room_id_, sender);�������úw.�������������?¬…±�������        if (with_sender) {�
������úw.�������
������?¬…±��������        generateTimestamp(timestamp);��                           timestamp};�                           utils::descriptiveTime(timestamp),�                           " sent a notification",�                           sender,�                           Cache::displayName(room_id_, sender),�        descriptionMsg_ = {event_id_,��        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.�������������?¬…±�������        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.�������������?¬…±��������        const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);����	���úw.����������	���?¬…±�������        const auto sender    = QString::fromStdString(event.sender);�������úw.�������������?¬…±�������        event_id_            = QString::fromStdString(event.event_id);��        markOwnMessagesAsReceived(event.sender);��        addReplyAction();�	������úw.�������	������?¬…±�������        init();�	������úw.�������	������?¬…±�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Notice)�  : QWidget(parent)�                           QWidget *parent)�������?¬…±�������%������úw.�������%������?¬…±�������                           const QString &room_id,�"������úw.�������"������?¬…±�������+������úw.�������+������?¬…±�������                           bool with_sender,�!������úw.�������!������?¬…±�������TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,�������úw.�������������?¬…±�������������úw.�������������?¬…±�������T������úw.�������T������?¬…±������� */� * Used to display remote notice messages.�/*��}�        markOwnMessagesAsReceived(event.sender);��          video, event, with_sender);�        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(�{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Video)�  : QWidget(parent)�                           QWidget *parent)�������?¬…±�������%������úw.�������%������?¬…±�������                           const QString &room_id,�"������úw.�������"������?¬…±�������+������úw.�������+������?¬…±�������                           bool with_sender,�!������úw.�������!������?¬…±�������                           const mtx::events::RoomEvent<mtx::events::msg::Video> &event,�S������úw.�������S������?¬…±�������TimelineItem::TimelineItem(VideoItem *video,�������úw.�������������?¬…±�������������úw.	�������������?¬…±	����������	���úw.����������	���?¬…±�������'������úw.�������'������?¬…±��������}�        markOwnMessagesAsReceived(event.sender);��          audio, event, with_sender);�        setupWidgetLayout<mtx::events::RoomEvent<mtx::even                           QWidget *parent)�������úw.�������������?¬…±�������%������úw.�������                           QWidget *pare                           QWidget *pare                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������ad��r��Z�����3�������Ö��Ò��=��ü��¡��M��9����ð
��î
��Ê
��œ
��›
��j
��i
��%
��Ï��Î��]��å��ä��}��(��á
-��à
-��º
-��›
-��x
-��.
-��ð	��É	��È	��¢	��¡	��r	��/	��ÿ��þ��×��Æ��˜��_��U��T��!��������ò��î��Z��’��ž��r��^��,��������ê��é��¸��·��s��1��0��Ó��o��n����Ú��Ù��©��ƒ��(����Ï��‘��j��i��C��B��'��ä��´��³��Œ��{��M��(������þ��ü��û��Ý��Û��·��Š��ˆ��‡��‚��i��g��'��%��$����Ü��Ú��í��������{�TimelineItem::markOwnMessagesAsReceived(const std::string &sender)�void��}�        statusIndicator_->setState(StatusIndicatorState::Sent);�{�TimelineItem::markSent()�void��}�        colorGenerating_->waitForFinished();�        colorGenerating_->cancel();�{�TimelineItem::~TimelineItem()��}�        adjustMessageLayout();��        }�                setupSimpleLayout();�                generateBody(formatted_body);�        } else {�                setUserAvatar(sender);��                setupAvatarLayout(displayName);�                generateBody(sender, displayName, formatted_body);�        if (with_sender) {��        generateTimestamp(timestamp);��                           timestamp};�                           utils::descriptiveTime(timestamp),�                           QString(": %1").arg(body),�                           sender,�                           sender == settings.value("auth/user_id") ? "You" : displayName,�        descriptionMsg_ = {event_id_,�        QSettTimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx:TimelineItem::TimeliTimelineItem::TimelineItem(const mtx::evTimelineItem::TimelineItem(const mtx::evTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimeliTimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx:TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event,�������úw.�������������úw.	�������R������úw.������� */� * Used to display remote text messages.�/*��}�        adjustMessageLayout();�	������úw.��������        }�                setupSimpleLayout();�������úw.�������                generateBody(formatted_body);�        } else {�                setUserAvatar(sender);��                setupAvatarLayout(displayName);�                generateBody(sender, displayName, formatted_body);�        if (with_sender) {�
������úw.��������        generateTimestamp(timestamp);��                           timestamp};�                           utils::descriptiveTime(timestamp),�                           QString("* %1 %2").arg(displayName).arg(body),�                           sender,�                           "",�        descriptionMsg_ = {event_id_,��        formatted_body   = QString("<em>%1</em>").arg(formatted_body);�        auto displayName = Cache::displayName(room_id_, sender);�������úw.�������        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);����	���úw.��������        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.�������        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.��������        const auto sender = QString::fromStdString(event.sender);�������úw.�������        event_id_         = QString::fromStdString(event.event_id);��        markOwnMessagesAsReceived(event.sender);��        addReplyAction();�	������úw.�������        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Emote)�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool with_sender,�!������úw.�������TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event,�������úw.�������������úw.�������S������úw.������� */� * Used to display remote emote messages.�ad��¤��¬�����;�������¿��d����ü��Ê��´��²��Ž��`��_��.��-��é
��“
��’
��!
��©��¨��A��ì��ë��§����&����Í
-��
-��h
-��g
-��A
-��@
-��
-��Î	��ž	��	��v	��e	��7	��þ��ô��ó��À��¾��½��c��a��=������
����Ç��Å��5��3��2��-��®��¬������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{�TimelineItem::mark{�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::mark{�TimelineItem::markOwnMessagesAsReceived(const std::string {�TimelineItem::markOwnMessagesAsReceive{�TimelineItem::markOwnMessagesAsReceived(const std::string &sender)�������úw.{�TimelineItem::markOwnMessagesAsReceived(const std::string {�TimelineItem::markOwnMessagesAsReceived(const std::string &sender)�������úw.�������������úw.�������<������úw.�������void��}�        statusIndicator_->setState(StatusIndicatorState::Sent);�	������úw.
-�������������úw.�������$������úw.�������:������úw.�������{�TimelineItem::markSent()�������úw.�������������úw.�������void��}�        colorGenerating_->waitForFinished();�        colorGenerating_->cancel();�{�TimelineItem::~TimelineItem()�������úw.�������������úw.�������������úw.��������}�        adjustMessageLayout();�	������úw.��������        }�                setupSimpleLayout();�������úw.�������                generateBody(formatted_body);�        } else {�                setUserAvatar(sender);��                setupAvatarLayout(displayName);�                generateBody(sender, displayName, formatted_body);�        if (with_sender) {�
������úw.��������        generateTimestamp(timestamp);��                           timestamp};�                           utils::descriptiveTime(timestamp),�                           QString(": %1").arg(body),�                           sender,�                           sender == settings.value("auth/user_id") ? "You" : displayName,�        descriptionMsg_ = {event_id_,�        QSettings settings;�	���	���úw.�������������úw.��������        auto displayName = Cache::displayName(room_id_, sender);�������úw.�������        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);����	���úw.��������        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.�������        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.��������        const auto sender = QString::fromStdString(event.sender);�������úw.�������        event_id_         = QString::fromStdString(event.event_id);��        markOwnMessagesAsReceived(event.sender);��        addReplyAction();�	������úw.�������        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Text)�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool with_sender,�!������úw.�������ad��J��&�����0�������ì��ê��²��x��6��â��à��ß��Ú��-��+��ë
��ê
��Ÿ
��W
��
��
��Ó��Ñ��Ð��Ë��d��b��ó��Û��Ú��™��H��G��)��(��ú
-��§
-��}
-��f
-��\
-��
-��Ý	��Æ	��o	��A	��*	�� 	��Í��™��‚��x��&��h��Á��¿��¾��¹��÷��õ����Œ��K��á��´����4����ð��‹��c��L��ç��¿��¨��B��T��ƒ��y��x        case StatusIndicatorState::Empty:�������?¬…±�������$������úw.�������$������?¬…±�������                break;�                setToolTip(tr("Sent"));�        case StatusIndicatorState::Sent:�������?¬…±�������$������úw.�������$������?¬…±�������                break;�                setToolTip(tr("Seen"));�        case StatusIndicatorState::Read:�������?¬…±�������$������úw.�������$������?¬…±�������                break;�                setToolTip(tr("Delivered"));�        case StatusIndicatorState::Received:�������?¬…±�������$������úw.�������$������?¬…±�������                break;�                setToolTip(tr("Encrypted"));�        case StatusIndicatorState::Encrypted:�������?¬…±�������$���	���úw.�������$���	���?¬…±�������        switch (state) {�������úw.�������������?¬…±��������        state_ = state;�	������úw.
-�������	������?¬…±
-�������������úw.�������������?¬…±�������{�StatusIndicator::setState(StatusIndicatorState state)�������úw.�������������?¬…±�������������úw.������        case StatusIndicatorState::Empty:�������úw.�������������?¬…±�������$������úw.�������        case StatusIndicatorState::Empty:�������úw.�������������?¬…±�������$������úw.�������$������?¬…±�����        case StatusI        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty:�������úw.�����        case StatusIndicatorState::Empty:�������úw.�����        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusI        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty:�������úw.�������$������úw.�������        }�                break;�                paintIcon(p, doubleCheckmarkIcon_);�        case StatusIndicatorState::Read: {�������úw.�������$������úw.�������        }�                break;�                paintIcon(p, checkmarkIcon_);�        case StatusIndicatorState::Received: {�������úw.�������$������úw.�������                break;�                paintIcon(p, lockIcon_);�        case StatusIndicatorState::Encrypted:�������úw.�������$���	���úw.�������        }�                break;�                paintIcon(p, clockIcon_);�        case StatusIndicatorState::Sent: {�������úw.�������$������úw.�������        switch (state_) {�������úw.
-��������        p.setPen(iconColor_);��        PainterHighQualityEnabler hq(p);�	������úw.�������#������úw.�������        Painter p(this);�	������úw.�������������úw.��������                return;�        if (state_ == StatusIndicatorState::Empty)�
������úw.
-�������������úw.�������-������úw.�������{�StatusIndicator::paintEvent(QPaintEvent *)�������úw.����������
-���úw.�������������úw.�������void��}�        QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal);��        painter.fillRect(pixmap.rect(), p.pen().color());�        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);�        QPainter painter(&pixmap);�	������úw.�������������úw.��������        auto pixmap = icon.pixmap(width());�������úw.�������{�StatusIndicator::paintIcon(QPainter &p, QIcon &icon)�������úw.����������	���úw.�������������úw.�������&������úw.�������)������úw.�������0������úw.�������void��}�        doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png");�        checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png");�        clockIcon_.addFile(":/icons/icons/ui/clock.png");�        lockIcon_.addFile(":/icons/icons/ui/lock.png");�{�  : QWidget(parent)�ad��±��±�����9�������þ��Ñ��¹��¸��v����²��±��‚����Y��X��9��7��6��1��ã
��á
��¿
��|
��8
��é��«��\��þ��©��R����î
-��ì
-��ë
-��æ
-��“
-��‘
-��F
-��ü	��û	��²	��R	��ö��À��´��³����}��i��0��Ò��£��¢��…��W�����Þ��Ê��É��±���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������                  tr                  try {��                  }�                                 tr                  try {��                                 try {��                                 tr                  tr                  tr                  tr                  try {��                  }�                          return;�                            "failed to retrieve event {} from {}", event_id, room_id);�                          nhlog::net()->warn(�                  if (err) {��                  using namespace mtx::events;�            const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) {�          [event_id, room_id, proxy = std::move(proxy)](�          event_id,�          room_id,�        http::client()->get_event(��        });�                Q_UNUSED(dialog);�������úw.
�������                auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))};�        connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) {�        auto proxy = std::make_shared<EventProxy>();�������úw.��������        const auto room_id  = room_id_.toStdString();�������úw.�������        const auto event_id = event_id_.toStdString();�������úw.�������{�TimelineItem::openRawMessageViewer() const�������úw.�������������úw.�������void��}�                                           });�                                                   }�                                                             event_id_.toStdString());�                                                             room_id_.toStdString(),�                                                             "failed to read_event ({}, {})",�                                                           nhlog::net()->warn(�                                                   if (err) {�                                           [this](mtx::http::RequestErr err) {�                                           event_id_.toStdString(),�                http::client()->read_event(room_id_.toStdString(),�        if (!event_id_.isEmpty())�{�TimelineItem::sendReadReceipt() const�������úw.�������������úw.�������void��}�        setUserAvatar(userid);��        setupAvatarLayout(displayName);��        generateUserName(userid, displayName);��        auto displayName = Cache::displayName(room_id_, userid);�������úw.�������        auto userid      = descriptionMsg_.userid;�������úw.�������������úw.
-�������,������úw.�������        // TODO: should be replaced with the proper event struct.��                return;�        if (userAvatar_)�
������úw.
-�������{�ad��Ò��î�����@�������û��¦��¤��o��B����á��¬��}��|��-��Ü
��–
��V
��
��
��â��à��ß��Ú��Ž��Œ��_��2����Ñ��œ��m��l����Ì
-��†
-��F
-��
-��
-��Ò	��Ð	��Ï	��Ê	��	��‹	��S	��	��ã��«��x��w��J��
-��Ë��ˆ��G����á��³��†��…��<����¿��³��²��V��î��N��U������Å��›��e��3�����¡��t����´��U��#��ÿ��þ��½��Ÿ��“��‚��©��������������������        connect(�        });�                          });�                                  emit eventRedacted(event_id_);��                                  }�                                          return;�                                                                   err->matrix_error.error)));�                                                                 .arg(QString::fromStdString(�                                          emit redactionFailed(tr("Message redaction failed: %1")�                                  if (err) {�                          [this](const mtx::responses::EventId &, mtx::http::RequestErr err)                 emit ChatPage::instance(                emit ChatPage::instance(                emit ChatPage::instance(                emit                emit                emit                emit                emit                emit                emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);�������úw.
�������        connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {��        });�                        MainWindow::instance()->openReadReceiptsDialog(event_id_);�                if (!event_id_.isEmpty())�        connect(showReadReceipts_, &QAction::triggered, this, [this]() {��        contextMenu_->addAction(redactMsg_);�        contextMenu_->addAction(markAsRead_);�        contextMenu_->addAction(viewRawMessage_);�        contextMenu_->addAction(showReadReceipts_);�        redactMsg_        = new QAction("Redact message", this);�        viewRawMessage_   = new QAction("View raw message", this);�        markAsRead_       = new QAction("Mark as read", this);�        showReadReceipts_ = new QAction("Read receipts", this);�        contextMenu_      = new QMenu(this);��        auto buttonSize_ = 32;�������úw.�������        body_            = nullptr;�	������úw.
-�������        userName_        = nullptr;�	���	���úw.
-�������        timestamp_       = nullptr;�	���
-���úw.
-�������        userAvatar_      = nullptr;�	������úw.
-�������{�TimelineItem::init()�������úw.�������������úw.�������void��}�        mainLayout_->addLayout(messageLayout_);��        messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);�        messageLayout_->setAlignment(timestamp_, Qt::AlignTop);�        messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);�        actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);�        actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);��        messageLayout_->addWidget(timestamp_);�        messageLayout_->addWidget(statusIndicator_);�        messageLayout_->addLayout(actionLayout_);�        actionLayout_->addWidget(contextBtn_);�        actionLayout_->addWidget(replyBtn_);�        messageLayout_->addWidget(body_, 1);�{�TimelineItem::adjustMessageLayout()�������úw.�������������úw.�������void��}�        mainLayout_->addLayout(messageLayout_);��        messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);�        messageLayout_->setAlignment(timestamp_, Qt::AlignTop);�        messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);�        actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);�        actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);��        messageLayout_->addWidget(timestamp_);�        messageLayout_->addWidget(statusIndicator_);�        messageLayout_->addLayout(actionLayout_);�        actionLayout_->addWidget(contextBtn_);�        actionLayout_->addWidget(replyBtn_);�        messageLayout_->addLayout(widgetLayout_, 1);�{�TimelineItem::adjustMessageLayoutForWidget()�������úw.�������������úw.�������void�ad�� ��������������ô��Ÿ��I��=��û��Ñ��›��i��6��×
��ª
��4
��Ö��w��E��!�� ��Ë��­��¡������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������        connect(�           connect(�           connect(�           connect(�        });�                          });�                                  emit eventRedacted(event_id_);�#������úw.
��������                                  }�                                          return;�                                                                   err->matrix_error.error)));�                                                                 .arg(QString::fromStdString(�                                          emit redactionFailed(tr("Message redaction failed: %1")�+������úw.
�������                                  if (err) {�                          [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {�                          event_id_.toStdString(),�                          room_id_.toStdString(),�                        http::client()->redact_event(�                if (!event_id_.isEmpty())�        connect(redactMsg_, &QAction::triggered, this, [this]() {�        });�                emit ChatPage::instance()->showNotification(msg);�������úw.
�������        connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {�        });�ad��u��É������������ÿ��Û��Ž��F��õ��ã����†��„��@��;��7
��5
��É�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������        generateUserName(user_id, displayname);�	������úw.�������	������?¬…±�������������úw.�������������?¬…±�������#������úw.�������#������?¬…±        generateUserName(user_id, displayname);�	������úw.        generateUserName(user_id, displayname);�	������úw.�������������úw.�������#������úw.�������{�TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body)�������úw.�������������úw.�������"������úw.�������+������úw.�������:������úw.�������C������úw.�������V������úw.�������_������úw.�������void�// The username/timestamp is displayed along with the message body.�}�        }�                userName_->setStyleSheet("QLabel { color : " + userColor + "; }");�                }�                        Cache::insertUserColor(userName_->toolTip(), userColor);�                if (Cache::userColor(userName_->toolTip()).isEmpty()) {�                // another TimelineItem might have inserted in the meantime.�        if (!userColor.isEmpty()) {��ad��Æ��B
������������þ��Æ��®��­��|��z��y��t��ë��é��Ï��—��•��”����+��)��ç
��Ï
��
��J
��H
��G
��B
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������void��}�        stylvoid��}�        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);�     void��}�        style()->drawPrimitive(QStyle::PE_Widget, &ovoid��}�        style()->drawPrimitive(Qvoid��}�        style()->drawPrimitive(Qvoid��}�        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);�        QPainter p(this);�	������úw.�������������úw.�������        opt.init(this);�        QStyleOption opt;�	������úw.�������������úw.�������{�TimelineItem::paintEvent(QPaintEvent *)�������úw.����������
-���úw.�������������úw.�������void��}�                contextMenu_->exec(event->globalPos());�        if (contextMenu_)�{�TimelineItem::contextMenuEvent(QContextMenuEvent *event)�������úw.�������������úw.������� ������úw.�������3������úw.�������void��}�        userAvatar_->setImage(room_id_, userid);��                return;�        if (userAvatar_ == nullptr)�
������úw.
-�������{�ad���������;�������Á��™��P��ü��û��Ï��Ÿ��b��ï
��î
��ª
��b
��6
��ë��¿��¾��u��F������»��º��„��;��ñ
-��ð
-��›
-��V
-��T
-��S
-��P
-��/
-��+
-��·	��]	��	��Ì��q����	��ó��Ý��Û��·��‰��ˆ��3��¾��½��‹��<��;����Ç������I��H���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������        if (ty == mtx::events::MessageType::Emote) {��              if (ty == mtx::events::MessageTy        if (ty == mtx::events::MessageTy        if (ty == mtx::events::MessageType::Emote) {��              if (ty == mtx::events::MessageTy        if (ty == mtx::events::MessageTy        if (ty == mt        if (ty == mtx::events::MessageTy        if (ty == mtx::events::MessageTy        if (ty == mt        if (ty == mt        if (ty == mt        if (ty == mtx::events::MessageType::Emote) {��              if (ty == mt        if (ty == mtx::events::MessageTy        if (ty == mtx::events::MessageType::Emote) {��        QString emptyEventId;�	������úw.�������������úw.��������                formatted_body = body.toHtmlEscaped();�        if (formatted_body == body.trimmed().toHtmlEscaped())�        // Escape html if the input is not formatted.��        auto formatted_body = utils::markdownToHtml(body);�������úw.�������        // Generate the html body to be rendered.��        auto timestamp   = QDateTime::currentDateTime();����	���úw.����������	���úw.�������'������úw.�������        auto displayName = Cache::displayName(room_id_, userid);�������úw.��������        addReplyAction();�	������úw.�������        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(ty)�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool withSender,�!���
-���úw.�������                           QString body,�������úw.�������$������úw.�������                           const QString &userid,�"������úw.�������+������úw.�������TimelineItem::TimelineItem(mtx::events::MessageType ty,�������úw.�������������úw.	�������5������úw.������� */� * For messages created locally.�/*��}�        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);�        parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);��        statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());�        statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());�        statusIndicator_ = new StatusIndicator(this);��        QFontMetrics tsFm(timestampFont_);�	������úw.�������������úw.��������        timestampFont_.setStyleHint(QFont::Monospace);�        timestampFont_.setFamily("Monospace");�        timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);��        contextBtn_->setMenu(contextMenu_);�        contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));�        contextBtn_->setIcon(context_icon);�        context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");�        QIcon context_icon;�	������úw.�������������úw.��������        contextBtn_->setCornerRadius(buttonSize_ / 2);�	������úw.
-�������������úw.�������&������úw.�������        contextBtn_->setFixedSize(buttonSize_, buttonSize_);�        contextBtn_->setToolTip(tr("Options"));�        contextBtn_ = new FlatButton(this);��        connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction);�        replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));�        replyBtn_->setIcon(reply_icon);�        reply_icon.addFile(":/icons/icons/ui/mail-reply.png");�ad��Í��Ù�����<�������þ��ý��ø������]��\��/��’��…��é
��è
��¹
��·
��¶
��—
��’
��
��
��Ð��p��o����Ò��t��h��f��e��`������Ð
-��£
-��w
-��B
-��8
-��
-��Ú	��›	��K	���	��Ö��Ã��Â��N��M����Ø��†��m�������ö��ô��ó��î��ž��œ��9��Ù�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������        QString userColor = colorGenerating_->result();�	���        QString user        QString user        QString userColor = colorGenerating_->result();�	������úw.�������	���        QString userColor = colorGenerating_->result();�	������úw.�������	���        QString user        QString userColor = colorGenerating_->result();�	������úw.�������	���        QString userColor = colorGenerat        QString user        QString userColor = colorGenerat        QString userColor = colorGenerat        QString userColor = colorGenerat        QString userColor = colorGenerating_->result();�	������úw.����������	���úw.�������        nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString());�{�TimelineItem::finishedGeneratingColor()�������úw.�������������úw.�������void��}�        }�                }�                        userName_->setStyleSheet("QLabel { color : " + userColor + "; }");�                } else {�                        colorGenerating_->setFuture(QtConcurrent::run(generate));�                if (userColor.isEmpty()) {�                // If the color is empty, then generate it asynchronously��                QString userColor = Cache::userColor(userName_->toolTip());�������úw.����������	���úw.��������                };�                        return userColor;�                          userName_->toolTip(), backgroundColor().name());�                        QString userColor = utils::generateContrastingHexColor(�                std::function<QString()> generate = [this]() {�                // generate user's unique color.�        if (userName_) {�
���	���úw.
-�������        }�                colorGenerating_->waitForFinished();�                colorGenerating_->cancel();�        if (colorGenerating_->isRunning()) {�        // Cancel and wait if we are already generating the color.�{�TimelineItem::refreshAuthorColor()�������úw.�������������úw.�������void��}�        });�                                                        ChatPage::instance()->currentRoom());�                MainWindow::instance()->openUserProfile(user_id,�        connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {��        body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);�        body_ = new TextLabel(utils::replaceEmoji(body), this);�{�TimelineItem::generateBody(const QString &body)�������úw.�������������úw.�������"������úw.�������+������úw.�������void�// Only the body is displayed.��}�        sendReadReceipt();�	������úw.��������                statusIndicator_->setState(StatusIndicatorState::Received);�������úw.
-�������#������úw.�������,������úw.�������B������úw.�������        else�                statusIndicator_->setState(StatusIndicatorState::Encrypted);�������úw.
-�������#������úw.�������,������úw.�������B���	���úw.�������        if (isEncrypted)�
������úw.��������        isReceived_ = true;�	������úw.
-�������{�TimelineItem::markReceived(bool isEncrypted)�������úw.�������������úw.�������!������úw.�������void��}�ad��q��©������������à��É��¿��¾��¬��ª��©������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������}�        update();��        }�                break;�                setToolTip("");�ad����C�����I�������ì��¹��£��¡��J��$��#��ò��ð��ï��r����Ä
��i
��
��
��Î��¸��¶��_��9��8����������Ö��Ò��<��û
-�� 
-��L
-��8
-��
-��î	��ì	��È	��š	��™	��h	��g	�� 	��Ç��\��[��ê��r��q��K��
-��ç��´��v��O��N��(��'��ø��›��š��W��'��&��ÿ��î��À��‡��}��|��I��G��F��C�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/*��}�        adjustMessageLayout();�	������úw.�������	������?¬…±��������  /*��}�        adjust/*��}�        adjust/*��}�        adjustMessageLayout();�	��/*��}�        adjustMessageLayout();�	��/*��}�        adjustMessageLayout();�	������úw.�������	��/*��}�        adjust/*��}�        adjustMessageLayout();�	��/*��}�        adjustMessageLayout();�	��/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjust/*��}�        adjustMessageLayout();�	������úw.��������        }�                setupSimpleLayout();�������úw.�������                generateBody(formatted_body);�        } else {�                setUserAvatar(sender);��                setupAvatarLayout(displayName);�                generateBody(sender, displayName, formatted_body);��                auto displayName = Cache::displayName(room_id_, sender);�������úw.�������        if (with_sender) {�
������úw.��������        generateTimestamp(timestamp);��                           timestamp};�                           utils::descriptiveTime(timestamp),�                           " sent a notification",�                           sender,�                           Cache::displayName(room_id_, sender),�        descriptionMsg_ = {event_id_,��        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();�������úw.�������        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());�������úw.��������        const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);����	���úw.�������        const auto sender    = QString::fromStdString(event.sender);�������úw.�������        event_id_            = QString::fromStdString(event.event_id);��        markOwnMessagesAsReceived(event.sender);��        addReplyAction();�	������úw.�������        init();�	������úw.�������{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Notice)�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool with_sender,�!������úw.�������TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,�������úw.�������������úw.�������T������úw.������� */� * Used to display remote notice messages.�/*��}�        markOwnMessagesAsReceived(event.sender);��          video, event, with_sender);�        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(�{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Video)�  : QWidget(parent)�                           QWidget *parent)�������úw.�������%������úw.�������                           const QString &room_id,�"������úw.�������+������úw.�������                           bool with_sender,�!������úw.�������                           const mtx::events::RoomEvent<mtx::events::msg::Video> &event,�S������úw.�������TimelineItem::TimelineItem(VideoItem *video,�������úw.�������������úw.	����������	���úw.�������'������úw.��������}�        markOwnMessagesAsReceived(event.sender);��          audio, event, with_sender);�        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(�{�  , room_id_{room_id}�  , message_type_(mtx::events::MessageType::Audio)�  : QWidget(parent)�ad��Ä��<������������é��ß��Ý��Ü��×��Q��O������á��‹��^��G��ò
��Å
��®
��]
��5
��
��Í��¥��Ž��<��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������        case StatusIndicatorState::Empty:�������úw.�������������?¬…±�����        case StatusIndicatorState::Empty        case StatusI        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty        case StatusIndicatorState::Empty:�������úw.�������$������úw.�������                break;�                setToolTip(tr("Sent"));�        case StatusIndicatorState::Sent:�������úw.�������$������úw.�������                break;�                setToolTip(tr("Seen"));�        case StatusIndicatorState::Read:�������úw.�������$������úw.�������                break;�                setToolTip(tr("Delivered"));�        case StatusIndicatorState::Received:�������úw.�������$������úw.�������                break;�                setToolTip(tr("Encrypted"));�        case StatusIndicatorState::Encrypted:�������úw.�������$���	���úw.�������        switch (state) {�������úw.��������        state_ = state;�	������úw.
-�������������úw.�������{�StatusIndicator::setState(StatusIndicatorState state)�������úw.�������������úw.�������������úw.�������0������úw.�������void��}�        }�                break;�
\ No newline at end of file
diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..632a2a646eeaa2cd7090bf0753268b0ffdc4a7e3
--- /dev/null
+++ b/src/timeline/DelegateChooser.cpp
@@ -0,0 +1,138 @@
+#include "DelegateChooser.h"
+
+#include "Logging.h"
+
+// uses private API, which moved between versions
+#include <QQmlEngine>
+#include <QtGlobal>
+
+QQmlComponent *
+DelegateChoice::delegate() const
+{
+        return delegate_;
+}
+
+void
+DelegateChoice::setDelegate(QQmlComponent *delegate)
+{
+        if (delegate != delegate_) {
+                delegate_ = delegate;
+                emit delegateChanged();
+                emit changed();
+        }
+}
+
+QVariant
+DelegateChoice::roleValue() const
+{
+        return roleValue_;
+}
+
+void
+DelegateChoice::setRoleValue(const QVariant &value)
+{
+        if (value != roleValue_) {
+                roleValue_ = value;
+                emit roleValueChanged();
+                emit changed();
+        }
+}
+
+QVariant
+DelegateChooser::roleValue() const
+{
+        return roleValue_;
+}
+
+void
+DelegateChooser::setRoleValue(const QVariant &value)
+{
+        if (value != roleValue_) {
+                roleValue_ = value;
+                recalcChild();
+                emit roleValueChanged();
+        }
+}
+
+QQmlListProperty<DelegateChoice>
+DelegateChooser::choices()
+{
+        return QQmlListProperty<DelegateChoice>(this,
+                                                this,
+                                                &DelegateChooser::appendChoice,
+                                                &DelegateChooser::choiceCount,
+                                                &DelegateChooser::choice,
+                                                &DelegateChooser::clearChoices);
+}
+
+void
+DelegateChooser::appendChoice(QQmlListProperty<DelegateChoice> *p, DelegateChoice *c)
+{
+        DelegateChooser *dc = static_cast<DelegateChooser *>(p->object);
+        dc->choices_.append(c);
+}
+
+int
+DelegateChooser::choiceCount(QQmlListProperty<DelegateChoice> *p)
+{
+        return static_cast<DelegateChooser *>(p->object)->choices_.count();
+}
+DelegateChoice *
+DelegateChooser::choice(QQmlListProperty<DelegateChoice> *p, int index)
+{
+        return static_cast<DelegateChooser *>(p->object)->choices_.at(index);
+}
+void
+DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p)
+{
+        static_cast<DelegateChooser *>(p->object)->choices_.clear();
+}
+
+void
+DelegateChooser::recalcChild()
+{
+        for (const auto choice : choices_) {
+                auto choiceValue = choice->roleValue();
+                if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) {
+                        if (child) {
+                                child->setParentItem(nullptr);
+                                child = nullptr;
+                        }
+
+                        choice->delegate()->create(incubator, QQmlEngine::contextForObject(this));
+                        return;
+                }
+        }
+}
+
+void
+DelegateChooser::componentComplete()
+{
+        QQuickItem::componentComplete();
+        recalcChild();
+}
+
+void
+DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status)
+{
+        if (status == QQmlIncubator::Ready) {
+                chooser.child = dynamic_cast<QQuickItem *>(object());
+                if (chooser.child == nullptr) {
+                        nhlog::ui()->error("Delegate has to be derived of Item!");
+                        return;
+                }
+
+                chooser.child->setParentItem(&chooser);
+                connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() {
+                        chooser.setHeight(chooser.child->height());
+                });
+                chooser.setHeight(chooser.child->height());
+                QQmlEngine::setObjectOwnership(chooser.child,
+                                               QQmlEngine::ObjectOwnership::JavaScriptOwnership);
+
+        } else if (status == QQmlIncubator::Error) {
+                for (const auto &e : errors())
+                        nhlog::ui()->error("Error instantiating delegate: {}",
+                                           e.toString().toStdString());
+        }
+}
diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h
new file mode 100644
index 0000000000000000000000000000000000000000..68ebeb0418ae88291e8c35ee551a83d9fa18bdea
--- /dev/null
+++ b/src/timeline/DelegateChooser.h
@@ -0,0 +1,82 @@
+// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt
+// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent
+
+#pragma once
+
+#include <QQmlComponent>
+#include <QQmlIncubator>
+#include <QQmlListProperty>
+#include <QQuickItem>
+#include <QtCore/QObject>
+#include <QtCore/QVariant>
+
+class QQmlAdaptorModel;
+
+class DelegateChoice : public QObject
+{
+        Q_OBJECT
+        Q_CLASSINFO("DefaultProperty", "delegate")
+
+public:
+        Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged)
+        Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
+
+        QQmlComponent *delegate() const;
+        void setDelegate(QQmlComponent *delegate);
+
+        QVariant roleValue() const;
+        void setRoleValue(const QVariant &value);
+
+signals:
+        void delegateChanged();
+        void roleValueChanged();
+        void changed();
+
+private:
+        QVariant roleValue_;
+        QQmlComponent *delegate_ = nullptr;
+};
+
+class DelegateChooser : public QQuickItem
+{
+        Q_OBJECT
+        Q_CLASSINFO("DefaultProperty", "choices")
+
+public:
+        Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT)
+        Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged)
+
+        QQmlListProperty<DelegateChoice> choices();
+
+        QVariant roleValue() const;
+        void setRoleValue(const QVariant &value);
+
+        void recalcChild();
+        void componentComplete() override;
+
+signals:
+        void roleChanged();
+        void roleValueChanged();
+
+private:
+        struct DelegateIncubator : public QQmlIncubator
+        {
+                DelegateIncubator(DelegateChooser &parent)
+                  : QQmlIncubator(QQmlIncubator::AsynchronousIfNested)
+                  , chooser(parent)
+                {}
+                void statusChanged(QQmlIncubator::Status status) override;
+
+                DelegateChooser &chooser;
+        };
+
+        QVariant roleValue_;
+        QList<DelegateChoice *> choices_;
+        QQuickItem *child = nullptr;
+        DelegateIncubator incubator{*this};
+
+        static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *);
+        static int choiceCount(QQmlListProperty<DelegateChoice> *);
+        static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index);
+        static void clearChoices(QQmlListProperty<DelegateChoice> *);
+};
diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp
deleted file mode 100644
index 7916bd80c2efee1efef09b1d3f70772d3ad18802..0000000000000000000000000000000000000000
--- a/src/timeline/TimelineItem.cpp
+++ /dev/null
@@ -1,960 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-#include <functional>
-
-#include <QContextMenuEvent>
-#include <QDesktopServices>
-#include <QFontDatabase>
-#include <QMenu>
-#include <QTimer>
-#include <QtGlobal>
-
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "MainWindow.h"
-#include "Olm.h"
-#include "ui/Avatar.h"
-#include "ui/Painter.h"
-#include "ui/TextLabel.h"
-
-#include "timeline/TimelineItem.h"
-#include "timeline/widgets/AudioItem.h"
-#include "timeline/widgets/FileItem.h"
-#include "timeline/widgets/ImageItem.h"
-#include "timeline/widgets/VideoItem.h"
-
-#include "dialogs/RawMessage.h"
-#include "mtx/identifiers.hpp"
-
-constexpr int MSG_RIGHT_MARGIN = 7;
-constexpr int MSG_PADDING      = 20;
-
-StatusIndicator::StatusIndicator(QWidget *parent)
-  : QWidget(parent)
-{
-        lockIcon_.addFile(":/icons/icons/ui/lock.png");
-        clockIcon_.addFile(":/icons/icons/ui/clock.png");
-        checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png");
-        doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png");
-}
-
-void
-StatusIndicator::paintIcon(QPainter &p, QIcon &icon)
-{
-        auto pixmap = icon.pixmap(width());
-
-        QPainter painter(&pixmap);
-        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
-        painter.fillRect(pixmap.rect(), p.pen().color());
-
-        QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal);
-}
-
-void
-StatusIndicator::paintEvent(QPaintEvent *)
-{
-        if (state_ == StatusIndicatorState::Empty)
-                return;
-
-        Painter p(this);
-        PainterHighQualityEnabler hq(p);
-
-        p.setPen(iconColor_);
-
-        switch (state_) {
-        case StatusIndicatorState::Sent: {
-                paintIcon(p, clockIcon_);
-                break;
-        }
-        case StatusIndicatorState::Encrypted:
-                paintIcon(p, lockIcon_);
-                break;
-        case StatusIndicatorState::Received: {
-                paintIcon(p, checkmarkIcon_);
-                break;
-        }
-        case StatusIndicatorState::Read: {
-                paintIcon(p, doubleCheckmarkIcon_);
-                break;
-        }
-        case StatusIndicatorState::Empty:
-                break;
-        }
-}
-
-void
-StatusIndicator::setState(StatusIndicatorState state)
-{
-        state_ = state;
-
-        switch (state) {
-        case StatusIndicatorState::Encrypted:
-                setToolTip(tr("Encrypted"));
-                break;
-        case StatusIndicatorState::Received:
-                setToolTip(tr("Delivered"));
-                break;
-        case StatusIndicatorState::Read:
-                setToolTip(tr("Seen"));
-                break;
-        case StatusIndicatorState::Sent:
-                setToolTip(tr("Sent"));
-                break;
-        case StatusIndicatorState::Empty:
-                setToolTip("");
-                break;
-        }
-
-        update();
-}
-
-void
-TimelineItem::adjustMessageLayoutForWidget()
-{
-        messageLayout_->addLayout(widgetLayout_, 1);
-        actionLayout_->addWidget(replyBtn_);
-        actionLayout_->addWidget(contextBtn_);
-        messageLayout_->addLayout(actionLayout_);
-        messageLayout_->addWidget(statusIndicator_);
-        messageLayout_->addWidget(timestamp_);
-
-        actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);
-        actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);
-        messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);
-        messageLayout_->setAlignment(timestamp_, Qt::AlignTop);
-        messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);
-
-        mainLayout_->addLayout(messageLayout_);
-}
-
-void
-TimelineItem::adjustMessageLayout()
-{
-        messageLayout_->addWidget(body_, 1);
-        actionLayout_->addWidget(replyBtn_);
-        actionLayout_->addWidget(contextBtn_);
-        messageLayout_->addLayout(actionLayout_);
-        messageLayout_->addWidget(statusIndicator_);
-        messageLayout_->addWidget(timestamp_);
-
-        actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);
-        actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);
-        messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);
-        messageLayout_->setAlignment(timestamp_, Qt::AlignTop);
-        messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);
-
-        mainLayout_->addLayout(messageLayout_);
-}
-
-void
-TimelineItem::init()
-{
-        userAvatar_      = nullptr;
-        timestamp_       = nullptr;
-        userName_        = nullptr;
-        body_            = nullptr;
-        auto buttonSize_ = 32;
-
-        contextMenu_      = new QMenu(this);
-        showReadReceipts_ = new QAction("Read receipts", this);
-        markAsRead_       = new QAction("Mark as read", this);
-        viewRawMessage_   = new QAction("View raw message", this);
-        redactMsg_        = new QAction("Redact message", this);
-        contextMenu_->addAction(showReadReceipts_);
-        contextMenu_->addAction(viewRawMessage_);
-        contextMenu_->addAction(markAsRead_);
-        contextMenu_->addAction(redactMsg_);
-
-        connect(showReadReceipts_, &QAction::triggered, this, [this]() {
-                if (!event_id_.isEmpty())
-                        MainWindow::instance()->openReadReceiptsDialog(event_id_);
-        });
-
-        connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {
-                emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);
-        });
-        connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {
-                emit ChatPage::instance()->showNotification(msg);
-        });
-        connect(redactMsg_, &QAction::triggered, this, [this]() {
-                if (!event_id_.isEmpty())
-                        http::client()->redact_event(
-                          room_id_.toStdString(),
-                          event_id_.toStdString(),
-                          [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
-                                  if (err) {
-                                          emit redactionFailed(tr("Message redaction failed: %1")
-                                                                 .arg(QString::fromStdString(
-                                                                   err->matrix_error.error)));
-                                          return;
-                                  }
-
-                                  emit eventRedacted(event_id_);
-                          });
-        });
-        connect(
-          ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor);
-        connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt);
-        connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer);
-
-        colorGenerating_ = new QFutureWatcher<QString>(this);
-        connect(colorGenerating_,
-                &QFutureWatcher<QString>::finished,
-                this,
-                &TimelineItem::finishedGeneratingColor);
-
-        topLayout_     = new QHBoxLayout(this);
-        mainLayout_    = new QVBoxLayout;
-        messageLayout_ = new QHBoxLayout;
-        actionLayout_  = new QHBoxLayout;
-        messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0);
-        messageLayout_->setSpacing(MSG_PADDING);
-
-        actionLayout_->setContentsMargins(13, 1, 13, 0);
-        actionLayout_->setSpacing(0);
-
-        topLayout_->setContentsMargins(
-          conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0);
-        topLayout_->setSpacing(0);
-        topLayout_->addLayout(mainLayout_);
-
-        mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);
-        mainLayout_->setSpacing(0);
-
-        replyBtn_ = new FlatButton(this);
-        replyBtn_->setToolTip(tr("Reply"));
-        replyBtn_->setFixedSize(buttonSize_, buttonSize_);
-        replyBtn_->setCornerRadius(buttonSize_ / 2);
-
-        QIcon reply_icon;
-        reply_icon.addFile(":/icons/icons/ui/mail-reply.png");
-        replyBtn_->setIcon(reply_icon);
-        replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
-        connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction);
-
-        contextBtn_ = new FlatButton(this);
-        contextBtn_->setToolTip(tr("Options"));
-        contextBtn_->setFixedSize(buttonSize_, buttonSize_);
-        contextBtn_->setCornerRadius(buttonSize_ / 2);
-
-        QIcon context_icon;
-        context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");
-        contextBtn_->setIcon(context_icon);
-        contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
-        contextBtn_->setMenu(contextMenu_);
-
-        timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);
-        timestampFont_.setFamily("Monospace");
-        timestampFont_.setStyleHint(QFont::Monospace);
-
-        QFontMetrics tsFm(timestampFont_);
-
-        statusIndicator_ = new StatusIndicator(this);
-        statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());
-        statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());
-
-        parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-}
-
-/*
- * For messages created locally.
- */
-TimelineItem::TimelineItem(mtx::events::MessageType ty,
-                           const QString &userid,
-                           QString body,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(ty)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        auto displayName = Cache::displayName(room_id_, userid);
-        auto timestamp   = QDateTime::currentDateTime();
-
-        // Generate the html body to be rendered.
-        auto formatted_body = utils::markdownToHtml(body);
-
-        // Escape html if the input is not formatted.
-        if (formatted_body == body.trimmed().toHtmlEscaped())
-                formatted_body = body.toHtmlEscaped();
-
-        QString emptyEventId;
-
-        if (ty == mtx::events::MessageType::Emote) {
-                formatted_body  = QString("<em>%1</em>").arg(formatted_body);
-                descriptionMsg_ = {emptyEventId,
-                                   "",
-                                   userid,
-                                   QString("* %1 %2").arg(displayName).arg(body),
-                                   utils::descriptiveTime(timestamp),
-                                   timestamp};
-        } else {
-                descriptionMsg_ = {emptyEventId,
-                                   "You: ",
-                                   userid,
-                                   body,
-                                   utils::descriptiveTime(timestamp),
-                                   timestamp};
-        }
-
-        formatted_body = utils::linkifyMessage(formatted_body);
-        formatted_body.replace("mx-reply", "div");
-
-        generateTimestamp(timestamp);
-
-        if (withSender) {
-                generateBody(userid, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(userid);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-TimelineItem::TimelineItem(ImageItem *image,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::Image)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<ImageItem>(image, userid, withSender);
-
-        addSaveImageAction(image);
-}
-
-TimelineItem::TimelineItem(FileItem *file,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::File)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<FileItem>(file, userid, withSender);
-}
-
-TimelineItem::TimelineItem(AudioItem *audio,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::Audio)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<AudioItem>(audio, userid, withSender);
-}
-
-TimelineItem::TimelineItem(VideoItem *video,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::Video)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<VideoItem>(video, userid, withSender);
-}
-
-TimelineItem::TimelineItem(ImageItem *image,
-                           const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Image)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(
-          image, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-
-        addSaveImageAction(image);
-}
-
-TimelineItem::TimelineItem(StickerItem *image,
-                           const mtx::events::Sticker &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-
-        addSaveImageAction(image);
-}
-
-TimelineItem::TimelineItem(FileItem *file,
-                           const mtx::events::RoomEvent<mtx::events::msg::File> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::File)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(
-          file, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-}
-
-TimelineItem::TimelineItem(AudioItem *audio,
-                           const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Audio)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(
-          audio, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-}
-
-TimelineItem::TimelineItem(VideoItem *video,
-                           const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Video)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(
-          video, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-}
-
-/*
- * Used to display remote notice messages.
- */
-TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Notice)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        markOwnMessagesAsReceived(event.sender);
-
-        event_id_            = QString::fromStdString(event.event_id);
-        const auto sender    = QString::fromStdString(event.sender);
-        const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-
-        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
-        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
-
-        descriptionMsg_ = {event_id_,
-                           Cache::displayName(room_id_, sender),
-                           sender,
-                           " sent a notification",
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        if (with_sender) {
-                auto displayName = Cache::displayName(room_id_, sender);
-
-                generateBody(sender, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-/*
- * Used to display remote emote messages.
- */
-TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Emote)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        markOwnMessagesAsReceived(event.sender);
-
-        event_id_         = QString::fromStdString(event.event_id);
-        const auto sender = QString::fromStdString(event.sender);
-
-        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
-        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
-
-        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-        auto displayName = Cache::displayName(room_id_, sender);
-        formatted_body   = QString("<em>%1</em>").arg(formatted_body);
-
-        descriptionMsg_ = {event_id_,
-                           "",
-                           sender,
-                           QString("* %1 %2").arg(displayName).arg(body),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        if (with_sender) {
-                generateBody(sender, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-/*
- * Used to display remote text messages.
- */
-TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Text)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        markOwnMessagesAsReceived(event.sender);
-
-        event_id_         = QString::fromStdString(event.event_id);
-        const auto sender = QString::fromStdString(event.sender);
-
-        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
-        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
-
-        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-        auto displayName = Cache::displayName(room_id_, sender);
-
-        QSettings settings;
-        descriptionMsg_ = {event_id_,
-                           sender == settings.value("auth/user_id") ? "You" : displayName,
-                           sender,
-                           QString(": %1").arg(body),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        if (with_sender) {
-                generateBody(sender, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-TimelineItem::~TimelineItem()
-{
-        colorGenerating_->cancel();
-        colorGenerating_->waitForFinished();
-}
-
-void
-TimelineItem::markSent()
-{
-        statusIndicator_->setState(StatusIndicatorState::Sent);
-}
-
-void
-TimelineItem::markOwnMessagesAsReceived(const std::string &sender)
-{
-        QSettings settings;
-        if (sender == settings.value("auth/user_id").toString().toStdString())
-                statusIndicator_->setState(StatusIndicatorState::Received);
-}
-
-void
-TimelineItem::markRead()
-{
-        if (statusIndicator_->state() != StatusIndicatorState::Encrypted)
-                statusIndicator_->setState(StatusIndicatorState::Read);
-}
-
-void
-TimelineItem::markReceived(bool isEncrypted)
-{
-        isReceived_ = true;
-
-        if (isEncrypted)
-                statusIndicator_->setState(StatusIndicatorState::Encrypted);
-        else
-                statusIndicator_->setState(StatusIndicatorState::Received);
-
-        sendReadReceipt();
-}
-
-// Only the body is displayed.
-void
-TimelineItem::generateBody(const QString &body)
-{
-        body_ = new TextLabel(utils::replaceEmoji(body), this);
-        body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
-
-        connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {
-                MainWindow::instance()->openUserProfile(user_id,
-                                                        ChatPage::instance()->currentRoom());
-        });
-}
-
-void
-TimelineItem::refreshAuthorColor()
-{
-        // Cancel and wait if we are already generating the color.
-        if (colorGenerating_->isRunning()) {
-                colorGenerating_->cancel();
-                colorGenerating_->waitForFinished();
-        }
-        if (userName_) {
-                // generate user's unique color.
-                std::function<QString()> generate = [this]() {
-                        QString userColor = utils::generateContrastingHexColor(
-                          userName_->toolTip(), backgroundColor().name());
-                        return userColor;
-                };
-
-                QString userColor = Cache::userColor(userName_->toolTip());
-
-                // If the color is empty, then generate it asynchronously
-                if (userColor.isEmpty()) {
-                        colorGenerating_->setFuture(QtConcurrent::run(generate));
-                } else {
-                        userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
-                }
-        }
-}
-
-void
-TimelineItem::finishedGeneratingColor()
-{
-        nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString());
-        QString userColor = colorGenerating_->result();
-
-        if (!userColor.isEmpty()) {
-                // another TimelineItem might have inserted in the meantime.
-                if (Cache::userColor(userName_->toolTip()).isEmpty()) {
-                        Cache::insertUserColor(userName_->toolTip(), userColor);
-                }
-                userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
-        }
-}
-// The username/timestamp is displayed along with the message body.
-void
-TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body)
-{
-        generateUserName(user_id, displayname);
-        generateBody(body);
-}
-
-void
-TimelineItem::generateUserName(const QString &user_id, const QString &displayname)
-{
-        auto sender = displayname;
-
-        if (displayname.startsWith("@")) {
-                // TODO: Fix this by using a UserId type.
-                if (displayname.split(":")[0].split("@").size() > 1)
-                        sender = displayname.split(":")[0].split("@")[1];
-        }
-
-        QFont usernameFont;
-        usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1);
-        usernameFont.setWeight(QFont::Medium);
-
-        QFontMetrics fm(usernameFont);
-
-        userName_ = new QLabel(this);
-        userName_->setFont(usernameFont);
-        userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500)));
-        userName_->setToolTip(user_id);
-        userName_->setToolTipDuration(1500);
-        userName_->setAttribute(Qt::WA_Hover);
-        userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-        // width deprecated in 5.13:
-        userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text()));
-#else
-        userName_->setFixedWidth(
-          QFontMetrics(userName_->font()).horizontalAdvance(userName_->text()));
-#endif
-        // Set the user color asynchronously if it hasn't been generated yet,
-        // otherwise this will just set it.
-        refreshAuthorColor();
-
-        auto filter = new UserProfileFilter(user_id, userName_);
-        userName_->installEventFilter(filter);
-        userName_->setCursor(Qt::PointingHandCursor);
-
-        connect(filter, &UserProfileFilter::hoverOn, this, [this]() {
-                QFont f = userName_->font();
-                f.setUnderline(true);
-                userName_->setFont(f);
-        });
-
-        connect(filter, &UserProfileFilter::hoverOff, this, [this]() {
-                QFont f = userName_->font();
-                f.setUnderline(false);
-                userName_->setFont(f);
-        });
-
-        connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() {
-                MainWindow::instance()->openUserProfile(user_id, room_id_);
-        });
-}
-
-void
-TimelineItem::generateTimestamp(const QDateTime &time)
-{
-        timestamp_ = new QLabel(this);
-        timestamp_->setFont(timestampFont_);
-        timestamp_->setText(
-          QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));
-}
-
-void
-TimelineItem::setupAvatarLayout(const QString &userName)
-{
-        topLayout_->setContentsMargins(
-          conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0);
-
-        QFont f;
-        f.setPointSizeF(f.pointSizeF());
-
-        userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2);
-        userAvatar_->setLetter(QChar(userName[0]).toUpper());
-
-        // TODO: The provided user name should be a UserId class
-        if (userName[0] == '@' && userName.size() > 1)
-                userAvatar_->setLetter(QChar(userName[1]).toUpper());
-
-        topLayout_->insertWidget(0, userAvatar_);
-        topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft);
-
-        if (userName_)
-                mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft);
-}
-
-void
-TimelineItem::setupSimpleLayout()
-{
-        QFont f;
-        f.setPointSizeF(f.pointSizeF());
-
-        topLayout_->setContentsMargins(conf::timeline::msgLeftMargin +
-                                         QFontMetrics(f).height() * 2 + 2,
-                                       conf::timeline::msgTopMargin,
-                                       0,
-                                       0);
-}
-
-void
-TimelineItem::setUserAvatar(const QString &userid)
-{
-        if (userAvatar_ == nullptr)
-                return;
-
-        userAvatar_->setImage(room_id_, userid);
-}
-
-void
-TimelineItem::contextMenuEvent(QContextMenuEvent *event)
-{
-        if (contextMenu_)
-                contextMenu_->exec(event->globalPos());
-}
-
-void
-TimelineItem::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-TimelineItem::addSaveImageAction(ImageItem *image)
-{
-        if (contextMenu_) {
-                auto saveImage = new QAction("Save image", this);
-                contextMenu_->addAction(saveImage);
-
-                connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs);
-        }
-}
-
-void
-TimelineItem::addReplyAction()
-{
-        if (contextMenu_) {
-                auto replyAction = new QAction("Reply", this);
-                contextMenu_->addAction(replyAction);
-
-                connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction);
-        }
-}
-
-void
-TimelineItem::replyAction()
-{
-        if (!body_)
-                return;
-
-        RelatedInfo related;
-        related.type          = message_type_;
-        related.quoted_body   = body_->toPlainText();
-        related.quoted_user   = descriptionMsg_.userid;
-        related.related_event = eventId().toStdString();
-        related.room          = room_id_;
-
-        emit ChatPage::instance()->messageReply(related);
-}
-
-void
-TimelineItem::addKeyRequestAction()
-{
-        if (contextMenu_) {
-                auto requestKeys = new QAction("Request encryption keys", this);
-                contextMenu_->addAction(requestKeys);
-
-                connect(requestKeys, &QAction::triggered, this, [this]() {
-                        olm::request_keys(room_id_.toStdString(), event_id_.toStdString());
-                });
-        }
-}
-
-void
-TimelineItem::addAvatar()
-{
-        if (userAvatar_)
-                return;
-
-        // TODO: should be replaced with the proper event struct.
-        auto userid      = descriptionMsg_.userid;
-        auto displayName = Cache::displayName(room_id_, userid);
-
-        generateUserName(userid, displayName);
-
-        setupAvatarLayout(displayName);
-
-        setUserAvatar(userid);
-}
-
-void
-TimelineItem::sendReadReceipt() const
-{
-        if (!event_id_.isEmpty())
-                http::client()->read_event(room_id_.toStdString(),
-                                           event_id_.toStdString(),
-                                           [this](mtx::http::RequestErr err) {
-                                                   if (err) {
-                                                           nhlog::net()->warn(
-                                                             "failed to read_event ({}, {})",
-                                                             room_id_.toStdString(),
-                                                             event_id_.toStdString());
-                                                   }
-                                           });
-}
-
-void
-TimelineItem::openRawMessageViewer() const
-{
-        const auto event_id = event_id_.toStdString();
-        const auto room_id  = room_id_.toStdString();
-
-        auto proxy = std::make_shared<EventProxy>();
-        connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) {
-                auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))};
-                Q_UNUSED(dialog);
-        });
-
-        http::client()->get_event(
-          room_id,
-          event_id,
-          [event_id, room_id, proxy = std::move(proxy)](
-            const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) {
-                  using namespace mtx::events;
-
-                  if (err) {
-                          nhlog::net()->warn(
-                            "failed to retrieve event {} from {}", event_id, room_id);
-                          return;
-                  }
-
-                  try {
-                          emit proxy->eventRetrieved(utils::serialize_event(res));
-                  } catch (const nlohmann::json::exception &e) {
-                          nhlog::net()->warn(
-                            "failed to serialize event ({}, {})", room_id, event_id);
-                  }
-          });
-}
diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h
deleted file mode 100644
index 356976e5931f140a6ec37d714f221b04af233619..0000000000000000000000000000000000000000
--- a/src/timeline/TimelineItem.h
+++ /dev/null
@@ -1,389 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QApplication>
-#include <QDateTime>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QLayout>
-#include <QPainter>
-#include <QSettings>
-#include <QTimer>
-
-#include <QtConcurrent>
-
-#include "mtx/events.hpp"
-
-#include "AvatarProvider.h"
-#include "RoomInfoListItem.h"
-#include "Utils.h"
-
-#include "Cache.h"
-#include "MatrixClient.h"
-
-#include "ui/FlatButton.h"
-
-class ImageItem;
-class StickerItem;
-class AudioItem;
-class VideoItem;
-class FileItem;
-class Avatar;
-class TextLabel;
-
-enum class StatusIndicatorState
-{
-        //! The encrypted message was received by the server.
-        Encrypted,
-        //! The plaintext message was received by the server.
-        Received,
-        //! At least one of the participants has read the message.
-        Read,
-        //! The client sent the message. Not yet received.
-        Sent,
-        //! When the message is loaded from cache or backfill.
-        Empty,
-};
-
-//!
-//! Used to notify the user about the status of a message.
-//!
-class StatusIndicator : public QWidget
-{
-        Q_OBJECT
-
-public:
-        explicit StatusIndicator(QWidget *parent);
-        void setState(StatusIndicatorState state);
-        StatusIndicatorState state() const { return state_; }
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-private:
-        void paintIcon(QPainter &p, QIcon &icon);
-
-        QIcon lockIcon_;
-        QIcon clockIcon_;
-        QIcon checkmarkIcon_;
-        QIcon doubleCheckmarkIcon_;
-
-        QColor iconColor_ = QColor("#999");
-
-        StatusIndicatorState state_ = StatusIndicatorState::Empty;
-
-        static constexpr int MaxWidth = 24;
-};
-
-class EventProxy : public QObject
-{
-        Q_OBJECT
-
-signals:
-        void eventRetrieved(const nlohmann::json &);
-};
-
-class UserProfileFilter : public QObject
-{
-        Q_OBJECT
-
-public:
-        explicit UserProfileFilter(const QString &user_id, QLabel *parent)
-          : QObject(parent)
-          , user_id_{user_id}
-        {}
-
-signals:
-        void hoverOff();
-        void hoverOn();
-        void clicked();
-
-protected:
-        bool eventFilter(QObject *obj, QEvent *event)
-        {
-                if (event->type() == QEvent::MouseButtonRelease) {
-                        emit clicked();
-                        return true;
-                } else if (event->type() == QEvent::HoverLeave) {
-                        emit hoverOff();
-                        return true;
-                } else if (event->type() == QEvent::HoverEnter) {
-                        emit hoverOn();
-                        return true;
-                }
-
-                return QObject::eventFilter(obj, event);
-        }
-
-private:
-        QString user_id_;
-};
-
-class TimelineItem : public QWidget
-{
-        Q_OBJECT
-        Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
-
-public:
-        TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-
-        // For local messages.
-        // m.text & m.emote
-        TimelineItem(mtx::events::MessageType ty,
-                     const QString &userid,
-                     QString body,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        // m.image
-        TimelineItem(ImageItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(FileItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(AudioItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(VideoItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-
-        TimelineItem(ImageItem *img,
-                     const mtx::events::RoomEvent<mtx::events::msg::Image> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(StickerItem *img,
-                     const mtx::events::Sticker &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(FileItem *file,
-                     const mtx::events::RoomEvent<mtx::events::msg::File> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(AudioItem *audio,
-                     const mtx::events::RoomEvent<mtx::events::msg::Audio> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(VideoItem *video,
-                     const mtx::events::RoomEvent<mtx::events::msg::Video> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-
-        ~TimelineItem();
-
-        void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        void setUserAvatar(const QString &userid);
-        DescInfo descriptionMessage() const { return descriptionMsg_; }
-        QString eventId() const { return event_id_; }
-        void setEventId(const QString &event_id) { event_id_ = event_id; }
-        void markReceived(bool isEncrypted);
-        void markRead();
-        void markSent();
-        bool isReceived() { return isReceived_; };
-        void setRoomId(QString room_id) { room_id_ = room_id; }
-        void sendReadReceipt() const;
-        void openRawMessageViewer() const;
-        void replyAction();
-
-        //! Add a user avatar for this event.
-        void addAvatar();
-        void addKeyRequestAction();
-
-signals:
-        void eventRedacted(const QString &event_id);
-        void redactionFailed(const QString &msg);
-
-public slots:
-        void refreshAuthorColor();
-        void finishedGeneratingColor();
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void contextMenuEvent(QContextMenuEvent *event) override;
-
-private:
-        //! If we are the sender of the message the event wil be marked as received by the server.
-        void markOwnMessagesAsReceived(const std::string &sender);
-        void init();
-        //! Add a context menu option to save the image of the timeline item.
-        void addSaveImageAction(ImageItem *image);
-        //! Add the reply action in the context menu for widgets that support it.
-        void addReplyAction();
-
-        template<class Widget>
-        void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender);
-
-        template<class Event, class Widget>
-        void setupWidgetLayout(Widget *widget, const Event &event, bool withSender);
-
-        void generateBody(const QString &body);
-        void generateBody(const QString &user_id, const QString &displayname, const QString &body);
-        void generateTimestamp(const QDateTime &time);
-        void generateUserName(const QString &userid, const QString &displayname);
-
-        void setupAvatarLayout(const QString &userName);
-        void setupSimpleLayout();
-
-        void adjustMessageLayout();
-        void adjustMessageLayoutForWidget();
-
-        //! Whether or not the event associated with the widget
-        //! has been acknowledged by the server.
-        bool isReceived_ = false;
-
-        QFutureWatcher<QString> *colorGenerating_;
-
-        QString event_id_;
-        mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown;
-        QString room_id_;
-
-        DescInfo descriptionMsg_;
-
-        QMenu *contextMenu_;
-        QAction *showReadReceipts_;
-        QAction *markAsRead_;
-        QAction *redactMsg_;
-        QAction *viewRawMessage_;
-        QAction *replyMsg_;
-
-        QHBoxLayout *topLayout_     = nullptr;
-        QHBoxLayout *messageLayout_ = nullptr;
-        QHBoxLayout *actionLayout_  = nullptr;
-        QVBoxLayout *mainLayout_    = nullptr;
-        QHBoxLayout *widgetLayout_  = nullptr;
-
-        Avatar *userAvatar_;
-
-        QFont timestampFont_;
-
-        StatusIndicator *statusIndicator_;
-
-        QLabel *timestamp_;
-        QLabel *userName_;
-        TextLabel *body_;
-
-        QColor backgroundColor_;
-
-        FlatButton *replyBtn_;
-        FlatButton *contextBtn_;
-};
-
-template<class Widget>
-void
-TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender)
-{
-        auto displayName = Cache::displayName(room_id_, userid);
-        auto timestamp   = QDateTime::currentDateTime();
-
-        descriptionMsg_ = {"", // No event_id up until this point.
-                           "You",
-                           userid,
-                           QString(" %1").arg(utils::messageDescription<Widget>()),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        widgetLayout_ = new QHBoxLayout;
-        widgetLayout_->setContentsMargins(0, 2, 0, 2);
-        widgetLayout_->addWidget(widget);
-        widgetLayout_->addStretch(1);
-
-        if (withSender) {
-                generateBody(userid, displayName, "");
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(userid);
-        } else {
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayoutForWidget();
-}
-
-template<class Event, class Widget>
-void
-TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender)
-{
-        init();
-
-        // if (event.type == mtx::events::EventType::RoomMessage) {
-        //        message_type_ = mtx::events::getMessageType(event.content.msgtype);
-        //}
-        // TODO: Fix this.
-        message_type_     = mtx::events::MessageType::Unknown;
-        event_id_         = QString::fromStdString(event.event_id);
-        const auto sender = QString::fromStdString(event.sender);
-
-        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-        auto displayName = Cache::displayName(room_id_, sender);
-
-        QSettings settings;
-        descriptionMsg_ = {event_id_,
-                           sender == settings.value("auth/user_id") ? "You" : displayName,
-                           sender,
-                           QString(" %1").arg(utils::messageDescription<Widget>()),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        widgetLayout_ = new QHBoxLayout();
-        widgetLayout_->setContentsMargins(0, 2, 0, 2);
-        widgetLayout_->addWidget(widget);
-        widgetLayout_->addStretch(1);
-
-        if (withSender) {
-                generateBody(sender, displayName, "");
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayoutForWidget();
-}
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e3d87ae6aa455b41c728db6255a7ece2d446f30a
--- /dev/null
+++ b/src/timeline/TimelineModel.cpp
@@ -0,0 +1,1541 @@
+#include "TimelineModel.h"
+
+#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"
+#include "dialogs/RawMessage.h"
+
+Q_DECLARE_METATYPE(QModelIndex)
+
+namespace {
+template<class T>
+QString
+eventId(const mtx::events::RoomEvent<T> &event)
+{
+        return QString::fromStdString(event.event_id);
+}
+template<class T>
+QString
+roomId(const mtx::events::Event<T> &event)
+{
+        return QString::fromStdString(event.room_id);
+}
+template<class T>
+QString
+senderId(const mtx::events::RoomEvent<T> &event)
+{
+        return QString::fromStdString(event.sender);
+}
+
+template<class T>
+QDateTime
+eventTimestamp(const mtx::events::RoomEvent<T> &event)
+{
+        return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
+}
+
+template<class T>
+std::string
+eventMsgType(const mtx::events::Event<T> &)
+{
+        return "";
+}
+template<class T>
+auto
+eventMsgType(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.msgtype)
+{
+        return e.content.msgtype;
+}
+
+template<class T>
+QString
+eventBody(const mtx::events::Event<T> &)
+{
+        return QString("");
+}
+template<class T>
+auto
+eventBody(const mtx::events::RoomEvent<T> &e)
+  -> std::enable_if_t<std::is_same<decltype(e.content.body), std::string>::value, QString>
+{
+        return QString::fromStdString(e.content.body);
+}
+
+template<class T>
+QString
+eventFormattedBody(const mtx::events::Event<T> &)
+{
+        return QString("");
+}
+template<class T>
+auto
+eventFormattedBody(const mtx::events::RoomEvent<T> &e)
+  -> std::enable_if_t<std::is_same<decltype(e.content.formatted_body), std::string>::value, QString>
+{
+        auto temp = e.content.formatted_body;
+        if (!temp.empty()) {
+                return QString::fromStdString(temp);
+        } else {
+                return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "<br>");
+        }
+}
+
+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);
+}
+
+template<class T>
+QString
+eventThumbnailUrl(const mtx::events::Event<T> &)
+{
+        return "";
+}
+template<class T>
+auto
+eventThumbnailUrl(const mtx::events::RoomEvent<T> &e)
+  -> std::enable_if_t<std::is_same<decltype(e.content.info.thumbnail_url), std::string>::value,
+                      QString>
+{
+        return QString::fromStdString(e.content.info.thumbnail_url);
+}
+
+template<class T>
+QString
+eventFilename(const mtx::events::Event<T> &)
+{
+        return "";
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e)
+{
+        // body may be the original filename
+        return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Video> &e)
+{
+        // body may be the original filename
+        return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Image> &e)
+{
+        // body may be the original filename
+        return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::File> &e)
+{
+        // body may be the original filename
+        if (!e.content.filename.empty())
+                return QString::fromStdString(e.content.filename);
+        return QString::fromStdString(e.content.body);
+}
+
+template<class T>
+auto
+eventFilesize(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.size)
+{
+        return e.content.info.size;
+}
+
+template<class T>
+int64_t
+eventFilesize(const mtx::events::Event<T> &)
+{
+        return 0;
+}
+
+template<class T>
+QString
+eventMimeType(const mtx::events::Event<T> &)
+{
+        return QString();
+}
+template<class T>
+auto
+eventMimeType(const mtx::events::RoomEvent<T> &e)
+  -> std::enable_if_t<std::is_same<decltype(e.content.info.mimetype), std::string>::value, QString>
+{
+        return QString::fromStdString(e.content.info.mimetype);
+}
+
+template<class T>
+QString
+eventRelatesTo(const mtx::events::Event<T> &)
+{
+        return QString();
+}
+template<class T>
+auto
+eventRelatesTo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<
+  std::is_same<decltype(e.content.relates_to.in_reply_to.event_id), std::string>::value,
+  QString>
+{
+        return QString::fromStdString(e.content.relates_to.in_reply_to.event_id);
+}
+
+template<class T>
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<T> &e)
+{
+        using mtx::events::EventType;
+        switch (e.type) {
+        case EventType::RoomKeyRequest:
+                return qml_mtx_events::EventType::KeyRequest;
+        case EventType::RoomAliases:
+                return qml_mtx_events::EventType::Aliases;
+        case EventType::RoomAvatar:
+                return qml_mtx_events::EventType::Avatar;
+        case EventType::RoomCanonicalAlias:
+                return qml_mtx_events::EventType::CanonicalAlias;
+        case EventType::RoomCreate:
+                return qml_mtx_events::EventType::Create;
+        case EventType::RoomEncrypted:
+                return qml_mtx_events::EventType::Encrypted;
+        case EventType::RoomEncryption:
+                return qml_mtx_events::EventType::Encryption;
+        case EventType::RoomGuestAccess:
+                return qml_mtx_events::EventType::GuestAccess;
+        case EventType::RoomHistoryVisibility:
+                return qml_mtx_events::EventType::HistoryVisibility;
+        case EventType::RoomJoinRules:
+                return qml_mtx_events::EventType::JoinRules;
+        case EventType::RoomMember:
+                return qml_mtx_events::EventType::Member;
+        case EventType::RoomMessage:
+                return qml_mtx_events::EventType::UnknownMessage;
+        case EventType::RoomName:
+                return qml_mtx_events::EventType::Name;
+        case EventType::RoomPowerLevels:
+                return qml_mtx_events::EventType::PowerLevels;
+        case EventType::RoomTopic:
+                return qml_mtx_events::EventType::Topic;
+        case EventType::RoomTombstone:
+                return qml_mtx_events::EventType::Tombstone;
+        case EventType::RoomRedaction:
+                return qml_mtx_events::EventType::Redaction;
+        case EventType::RoomPinnedEvents:
+                return qml_mtx_events::EventType::PinnedEvents;
+        case EventType::Sticker:
+                return qml_mtx_events::EventType::Sticker;
+        case EventType::Tag:
+                return qml_mtx_events::EventType::Tag;
+        case EventType::Unsupported:
+        default:
+                return qml_mtx_events::EventType::Unsupported;
+        }
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Audio> &)
+{
+        return qml_mtx_events::EventType::AudioMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Emote> &)
+{
+        return qml_mtx_events::EventType::EmoteMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::File> &)
+{
+        return qml_mtx_events::EventType::FileMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Image> &)
+{
+        return qml_mtx_events::EventType::ImageMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Notice> &)
+{
+        return qml_mtx_events::EventType::NoticeMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Text> &)
+{
+        return qml_mtx_events::EventType::TextMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Video> &)
+{
+        return qml_mtx_events::EventType::VideoMessage;
+}
+
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Redacted> &)
+{
+        return qml_mtx_events::EventType::Redacted;
+}
+// ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Location> &e) { return
+// ::EventType::LocationMessage; }
+
+template<class T>
+uint64_t
+eventHeight(const mtx::events::Event<T> &)
+{
+        return -1;
+}
+template<class T>
+auto
+eventHeight(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.h)
+{
+        return e.content.info.h;
+}
+template<class T>
+uint64_t
+eventWidth(const mtx::events::Event<T> &)
+{
+        return -1;
+}
+template<class T>
+auto
+eventWidth(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.w)
+{
+        return e.content.info.w;
+}
+
+template<class T>
+double
+eventPropHeight(const mtx::events::RoomEvent<T> &e)
+{
+        auto w = eventWidth(e);
+        if (w == 0)
+                w = 1;
+
+        double prop = eventHeight(e) / (double)w;
+
+        return prop > 0 ? prop : 1.;
+}
+}
+
+TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
+  : QAbstractListModel(parent)
+  , room_id_(room_id)
+  , manager_(manager)
+{
+        connect(
+          this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents);
+        connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) {
+                pending.removeOne(txn_id);
+                failed.insert(txn_id);
+                int idx = idToIndex(txn_id);
+                if (idx < 0) {
+                        nhlog::ui()->warn("Failed index out of range");
+                        return;
+                }
+                isProcessingPending = false;
+                emit dataChanged(index(idx, 0), index(idx, 0));
+        });
+        connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) {
+                pending.removeOne(txn_id);
+                int idx = idToIndex(txn_id);
+                if (idx < 0) {
+                        nhlog::ui()->warn("Sent index out of range");
+                        return;
+                }
+                eventOrder[idx] = event_id;
+                auto ev         = events.value(txn_id);
+                ev              = boost::apply_visitor(
+                  [event_id](const auto &e) -> mtx::events::collections::TimelineEvents {
+                          auto eventCopy     = e;
+                          eventCopy.event_id = event_id.toStdString();
+                          return eventCopy;
+                  },
+                  ev);
+                events.remove(txn_id);
+                events.insert(event_id, ev);
+
+                // mark our messages as read
+                readEvent(event_id.toStdString());
+
+                // ask to be notified for read receipts
+                cache::client()->addPendingReceipt(room_id_, event_id);
+
+                isProcessingPending = false;
+                emit dataChanged(index(idx, 0), index(idx, 0));
+
+                if (pending.size() > 0)
+                        emit nextPendingMessage();
+        });
+        connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) {
+                emit ChatPage::instance()->showNotification(msg);
+        });
+
+        connect(
+          this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage);
+        connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage);
+}
+
+QHash<int, QByteArray>
+TimelineModel::roleNames() const
+{
+        return {
+          {Section, "section"},
+          {Type, "type"},
+          {Body, "body"},
+          {FormattedBody, "formattedBody"},
+          {UserId, "userId"},
+          {UserName, "userName"},
+          {Timestamp, "timestamp"},
+          {Url, "url"},
+          {ThumbnailUrl, "thumbnailUrl"},
+          {Filename, "filename"},
+          {Filesize, "filesize"},
+          {MimeType, "mimetype"},
+          {Height, "height"},
+          {Width, "width"},
+          {ProportionalHeight, "proportionalHeight"},
+          {Id, "id"},
+          {State, "state"},
+          {IsEncrypted, "isEncrypted"},
+          {ReplyTo, "replyTo"},
+        };
+}
+int
+TimelineModel::rowCount(const QModelIndex &parent) const
+{
+        Q_UNUSED(parent);
+        return (int)this->eventOrder.size();
+}
+
+QVariant
+TimelineModel::data(const QModelIndex &index, int role) const
+{
+        if (index.row() < 0 && index.row() >= (int)eventOrder.size())
+                return QVariant();
+
+        QString id = eventOrder[index.row()];
+
+        mtx::events::collections::TimelineEvents event = events.value(id);
+
+        if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+                event = decryptEvent(*e).event;
+        }
+
+        switch (role) {
+        case Section: {
+                QDateTime date = boost::apply_visitor(
+                  [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event);
+                date.setTime(QTime());
+
+                QString userId =
+                  boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event);
+
+                for (int r = index.row() - 1; r > 0; r--) {
+                        auto tempEv        = events.value(eventOrder[r]);
+                        QDateTime prevDate = boost::apply_visitor(
+                          [](const auto &e) -> QDateTime { return eventTimestamp(e); }, tempEv);
+                        prevDate.setTime(QTime());
+                        if (prevDate != date)
+                                return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId);
+
+                        QString prevUserId = boost::apply_visitor(
+                          [](const auto &e) -> QString { return senderId(e); }, tempEv);
+                        if (userId != prevUserId)
+                                break;
+                }
+
+                return QString("%1").arg(userId);
+        }
+        case UserId:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return senderId(e); }, event));
+        case UserName:
+                return QVariant(displayName(boost::apply_visitor(
+                  [](const auto &e) -> QString { return senderId(e); }, event)));
+
+        case Timestamp:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event));
+        case Type:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); },
+                  event));
+        case Body:
+                return QVariant(utils::replaceEmoji(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventBody(e); }, event)));
+        case FormattedBody:
+                return QVariant(
+                  utils::replaceEmoji(
+                    utils::linkifyMessage(boost::apply_visitor(
+                      [](const auto &e) -> QString { return eventFormattedBody(e); }, event)))
+                    .remove("<mx-reply>")
+                    .remove("</mx-reply>"));
+        case Url:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventUrl(e); }, event));
+        case ThumbnailUrl:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event));
+        case Filename:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventFilename(e); }, event));
+        case Filesize:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString {
+                          return utils::humanReadableFileSize(eventFilesize(e));
+                  },
+                  event));
+        case MimeType:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventMimeType(e); }, event));
+        case Height:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> qulonglong { return eventHeight(e); }, event));
+        case Width:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> qulonglong { return eventWidth(e); }, event));
+        case ProportionalHeight:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> double { return eventPropHeight(e); }, event));
+        case Id:
+                return id;
+        case State:
+                // only show read receipts for messages not from us
+                if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); },
+                                         event)
+                      .toStdString() != http::client()->user_id().to_string())
+                        return qml_mtx_events::Empty;
+                else if (failed.contains(id))
+                        return qml_mtx_events::Failed;
+                else if (pending.contains(id))
+                        return qml_mtx_events::Sent;
+                else if (read.contains(id) ||
+                         cache::client()->readReceipts(id, room_id_).size() > 1)
+                        return qml_mtx_events::Read;
+                else
+                        return qml_mtx_events::Received;
+        case IsEncrypted: {
+                auto tempEvent = events[id];
+                return boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+                         &tempEvent) != nullptr;
+        }
+        case ReplyTo: {
+                QString evId = boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventRelatesTo(e); }, event);
+                return QVariant(evId);
+        }
+        default:
+                return QVariant();
+        }
+}
+
+void
+TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
+{
+        if (isInitialSync) {
+                prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
+                isInitialSync     = false;
+        }
+
+        if (timeline.events.empty())
+                return;
+
+        std::vector<QString> ids = internalAddEvents(timeline.events);
+
+        if (ids.empty())
+                return;
+
+        beginInsertRows(QModelIndex(),
+                        static_cast<int>(this->eventOrder.size()),
+                        static_cast<int>(this->eventOrder.size() + ids.size() - 1));
+        this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
+        endInsertRows();
+
+        updateLastMessage();
+}
+
+template<typename T>
+auto
+isMessage(const mtx::events::RoomEvent<T> &e)
+  -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool>
+{
+        return true;
+}
+
+template<typename T>
+auto
+isMessage(const mtx::events::Event<T> &)
+{
+        return false;
+}
+
+void
+TimelineModel::updateLastMessage()
+{
+        for (auto it = eventOrder.rbegin(); it != eventOrder.rend(); ++it) {
+                auto event = events.value(*it);
+                if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+                      &event)) {
+                        event = decryptEvent(*e).event;
+                }
+
+                if (!boost::apply_visitor([](const auto &e) -> bool { return isMessage(e); },
+                                          event))
+                        continue;
+
+                auto description = utils::getMessageDescription(
+                  event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
+                emit manager_->updateRoomsLastMessage(room_id_, description);
+                return;
+        }
+}
+
+std::vector<QString>
+TimelineModel::internalAddEvents(
+  const std::vector<mtx::events::collections::TimelineEvents> &timeline)
+{
+        std::vector<QString> ids;
+        for (const auto &e : timeline) {
+                QString id =
+                  boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e);
+
+                if (this->events.contains(id)) {
+                        this->events.insert(id, e);
+                        int idx = idToIndex(id);
+                        emit dataChanged(index(idx, 0), index(idx, 0));
+                        continue;
+                }
+
+                if (auto redaction =
+                      boost::get<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
+                        QString redacts = QString::fromStdString(redaction->redacts);
+                        auto redacted   = std::find(eventOrder.begin(), eventOrder.end(), redacts);
+
+                        if (redacted != eventOrder.end()) {
+                                auto redactedEvent = boost::apply_visitor(
+                                  [](const auto &ev)
+                                    -> mtx::events::RoomEvent<mtx::events::msg::Redacted> {
+                                          mtx::events::RoomEvent<mtx::events::msg::Redacted>
+                                            replacement                = {};
+                                          replacement.event_id         = ev.event_id;
+                                          replacement.room_id          = ev.room_id;
+                                          replacement.sender           = ev.sender;
+                                          replacement.origin_server_ts = ev.origin_server_ts;
+                                          replacement.type             = ev.type;
+                                          return replacement;
+                                  },
+                                  e);
+                                events.insert(redacts, redactedEvent);
+
+                                int row = (int)std::distance(eventOrder.begin(), redacted);
+                                emit dataChanged(index(row, 0), index(row, 0));
+                        }
+
+                        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);
+        }
+        return ids;
+}
+
+void
+TimelineModel::fetchHistory()
+{
+        if (paginationInProgress) {
+                nhlog::ui()->warn("Already loading older messages");
+                return;
+        }
+
+        paginationInProgress = true;
+        mtx::http::MessagesOpts opts;
+        opts.room_id = room_id_.toStdString();
+        opts.from    = prev_batch_token_.toStdString();
+
+        nhlog::ui()->info("Paginationg room {}", opts.room_id);
+
+        http::client()->messages(
+          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->error("failed to call /messages ({}): {} - {}",
+                                              opts.room_id,
+                                              mtx::errors::to_string(err->matrix_error.errcode),
+                                              err->matrix_error.error);
+                          paginationInProgress = false;
+                          return;
+                  }
+
+                  emit oldMessagesRetrieved(std::move(res));
+                  paginationInProgress = false;
+          });
+}
+
+void
+TimelineModel::setCurrentIndex(int index)
+{
+        auto oldIndex = idToIndex(currentId);
+        currentId     = indexToId(index);
+        emit currentIndexChanged(index);
+
+        if (oldIndex < index && !pending.contains(currentId) &&
+            ChatPage::instance()->isActiveWindow()) {
+                readEvent(currentId.toStdString());
+        }
+}
+
+void
+TimelineModel::readEvent(const std::string &id)
+{
+        http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) {
+                if (err) {
+                        nhlog::net()->warn("failed to read_event ({}, {})",
+                                           room_id_.toStdString(),
+                                           currentId.toStdString());
+                }
+        });
+}
+
+void
+TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
+{
+        std::vector<QString> ids = internalAddEvents(msgs.chunk);
+
+        if (!ids.empty()) {
+                beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1));
+                this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
+                endInsertRows();
+        }
+
+        prev_batch_token_ = QString::fromStdString(msgs.end);
+}
+
+QColor
+TimelineModel::userColor(QString id, QColor background)
+{
+        if (!userColors.contains(id))
+                userColors.insert(
+                  id, QColor(utils::generateContrastingHexColor(id, background.name())));
+        return userColors.value(id);
+}
+
+QString
+TimelineModel::displayName(QString id) const
+{
+        return Cache::displayName(room_id_, id);
+}
+
+QString
+TimelineModel::avatarUrl(QString id) const
+{
+        return Cache::avatarUrl(room_id_, id);
+}
+
+QString
+TimelineModel::formatDateSeparator(QDate date) const
+{
+        auto now = QDateTime::currentDateTime();
+
+        QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
+
+        if (now.date().year() == date.year()) {
+                QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
+                fmt = fmt.remove(rx);
+        }
+
+        return date.toString(fmt);
+}
+
+QString
+TimelineModel::escapeEmoji(QString str) const
+{
+        return utils::replaceEmoji(str);
+}
+
+void
+TimelineModel::viewRawMessage(QString id) const
+{
+        std::string ev = utils::serialize_event(events.value(id)).dump(4);
+        auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
+        Q_UNUSED(dialog);
+}
+
+void
+
+TimelineModel::openUserProfile(QString userid) const
+{
+        MainWindow::instance()->openUserProfile(userid, room_id_);
+}
+
+DecryptionResult
+TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const
+{
+        MegolmSessionIndex index;
+        index.room_id    = room_id_.toStdString();
+        index.session_id = e.content.session_id;
+        index.sender_key = e.content.sender_key;
+
+        mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
+        dummy.origin_server_ts = e.origin_server_ts;
+        dummy.event_id         = e.event_id;
+        dummy.sender           = e.sender;
+        dummy.content.body =
+          tr("-- Encrypted Event (No keys found for decryption) --",
+             "Placeholder, when the message was not decrypted yet or can't be decrypted")
+            .toStdString();
+
+        try {
+                if (!cache::client()->inboundMegolmSessionExists(index)) {
+                        nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
+                                              index.room_id,
+                                              index.session_id,
+                                              e.sender);
+                        // TODO: request megolm session_id & session_key from the sender.
+                        return {dummy, false};
+                }
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
+                dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
+                                        "Placeholder, when the message can't be decrypted, because "
+                                        "the DB access failed when trying to lookup the session.")
+                                       .toStdString();
+                return {dummy, false};
+        }
+
+        std::string msg_str;
+        try {
+                auto session = cache::client()->getInboundMegolmSession(index);
+                auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
+                msg_str      = std::string((char *)res.data.data(), res.data.size());
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
+                                      index.room_id,
+                                      index.session_id,
+                                      index.sender_key,
+                                      e.what());
+                dummy.content.body =
+                  tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
+                     "Placeholder, when the message can't be decrypted, because the DB access "
+                     "failed.")
+                    .toStdString();
+                return {dummy, false};
+        } catch (const mtx::crypto::olm_exception &e) {
+                nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
+                                          index.room_id,
+                                          index.session_id,
+                                          index.sender_key,
+                                          e.what());
+                dummy.content.body =
+                  tr("-- Decryption Error (%1) --",
+                     "Placeholder, when the message can't be decrypted. In this case, the Olm "
+                     "decrytion returned an error, which is passed ad %1")
+                    .arg(e.what())
+                    .toStdString();
+                return {dummy, false};
+        }
+
+        // Add missing fields for the event.
+        json body                = json::parse(msg_str);
+        body["event_id"]         = e.event_id;
+        body["sender"]           = e.sender;
+        body["origin_server_ts"] = e.origin_server_ts;
+        body["unsigned"]         = e.unsigned_data;
+
+        json event_array = json::array();
+        event_array.push_back(body);
+
+        std::vector<mtx::events::collections::TimelineEvents> temp_events;
+        mtx::responses::utils::parse_timeline_events(event_array, temp_events);
+
+        if (temp_events.size() == 1)
+                return {temp_events.at(0), true};
+
+        dummy.content.body =
+          tr("-- Encrypted Event (Unknown event type) --",
+             "Placeholder, when the message was decrypted, but we couldn't parse it, because "
+             "Nheko/mtxclient don't support that event type yet")
+            .toStdString();
+        return {dummy, false};
+}
+
+void
+TimelineModel::replyAction(QString id)
+{
+        auto event = events.value(id);
+        if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+                event = decryptEvent(*e).event;
+        }
+
+        RelatedInfo related = boost::apply_visitor(
+          [](const auto &ev) -> RelatedInfo {
+                  RelatedInfo related_   = {};
+                  related_.quoted_user   = QString::fromStdString(ev.sender);
+                  related_.related_event = ev.event_id;
+                  return related_;
+          },
+          event);
+        related.type        = mtx::events::getMessageType(boost::apply_visitor(
+          [](const auto &e) -> std::string { return eventMsgType(e); }, event));
+        related.quoted_body = boost::apply_visitor(
+          [](const auto &e) -> QString { return eventFormattedBody(e); }, event);
+        related.quoted_body.remove(QRegularExpression(
+          "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
+        nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString());
+        related.room = room_id_;
+
+        if (related.quoted_body.isEmpty())
+                return;
+
+        ChatPage::instance()->messageReply(related);
+}
+
+void
+TimelineModel::readReceiptsAction(QString id) const
+{
+        MainWindow::instance()->openReadReceiptsDialog(id);
+}
+
+void
+TimelineModel::redactEvent(QString id)
+{
+        if (!id.isEmpty())
+                http::client()->redact_event(
+                  room_id_.toStdString(),
+                  id.toStdString(),
+                  [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+                          if (err) {
+                                  emit redactionFailed(
+                                    tr("Message redaction failed: %1")
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+                                  return;
+                          }
+
+                          emit eventRedacted(id);
+                  });
+}
+
+int
+TimelineModel::idToIndex(QString id) const
+{
+        if (id.isEmpty())
+                return -1;
+        for (int i = 0; i < (int)eventOrder.size(); i++)
+                if (id == eventOrder[i])
+                        return i;
+        return -1;
+}
+
+QString
+TimelineModel::indexToId(int index) const
+{
+        if (index < 0 || index >= (int)eventOrder.size())
+                return "";
+        return eventOrder[index];
+}
+
+// Note: this will only be called for our messages
+void
+TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids)
+{
+        for (const auto &id : event_ids) {
+                read.insert(id);
+                int idx = idToIndex(id);
+                if (idx < 0) {
+                        nhlog::ui()->warn("Read index out of range");
+                        return;
+                }
+                emit dataChanged(index(idx, 0), index(idx, 0));
+        }
+}
+
+void
+TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content)
+{
+        const auto room_id = room_id_.toStdString();
+
+        using namespace mtx::events;
+        using namespace mtx::identifiers;
+
+        json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
+
+        try {
+                // Check if we have already an outbound megolm session then we can use.
+                if (cache::client()->outboundMegolmSessionExists(room_id)) {
+                        auto data = olm::encrypt_group_message(
+                          room_id, http::client()->device_id(), doc.dump());
+
+                        http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+                          room_id,
+                          txn_id,
+                          data,
+                          [this, txn_id](const mtx::responses::EventId &res,
+                                         mtx::http::RequestErr err) {
+                                  if (err) {
+                                          const int status_code =
+                                            static_cast<int>(err->status_code);
+                                          nhlog::net()->warn("[{}] failed to send message: {} {}",
+                                                             txn_id,
+                                                             err->matrix_error.error,
+                                                             status_code);
+                                          emit messageFailed(QString::fromStdString(txn_id));
+                                  }
+                                  emit messageSent(
+                                    QString::fromStdString(txn_id),
+                                    QString::fromStdString(res.event_id.to_string()));
+                          });
+                        return;
+                }
+
+                nhlog::ui()->debug("creating new outbound megolm session");
+
+                // Create a new outbound megolm session.
+                auto outbound_session  = olm::client()->init_outbound_group_session();
+                const auto session_id  = mtx::crypto::session_id(outbound_session.get());
+                const auto session_key = mtx::crypto::session_key(outbound_session.get());
+
+                // TODO: needs to be moved in the lib.
+                auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
+                                           {"room_id", room_id},
+                                           {"session_id", session_id},
+                                           {"session_key", session_key}};
+
+                // Saving the new megolm session.
+                // TODO: Maybe it's too early to save.
+                OutboundGroupSessionData session_data;
+                session_data.session_id    = session_id;
+                session_data.session_key   = session_key;
+                session_data.message_index = 0; // TODO Update me
+                cache::client()->saveOutboundMegolmSession(
+                  room_id, session_data, std::move(outbound_session));
+
+                const auto members = cache::client()->roomMembers(room_id);
+                nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
+
+                auto keeper =
+                  std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() {
+                          try {
+                                  auto data = olm::encrypt_group_message(
+                                    room_id, http::client()->device_id(), doc.dump());
+
+                                  http::client()
+                                    ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+                                      room_id,
+                                      txn_id,
+                                      data,
+                                      [this, txn_id](const mtx::responses::EventId &res,
+                                                     mtx::http::RequestErr err) {
+                                              if (err) {
+                                                      const int status_code =
+                                                        static_cast<int>(err->status_code);
+                                                      nhlog::net()->warn(
+                                                        "[{}] failed to send message: {} {}",
+                                                        txn_id,
+                                                        err->matrix_error.error,
+                                                        status_code);
+                                                      emit messageFailed(
+                                                        QString::fromStdString(txn_id));
+                                              }
+                                              emit messageSent(
+                                                QString::fromStdString(txn_id),
+                                                QString::fromStdString(res.event_id.to_string()));
+                                      });
+                          } catch (const lmdb::error &e) {
+                                  nhlog::db()->critical(
+                                    "failed to save megolm outbound session: {}", e.what());
+                                  emit messageFailed(QString::fromStdString(txn_id));
+                          }
+                  });
+
+                mtx::requests::QueryKeys req;
+                for (const auto &member : members)
+                        req.device_keys[member] = {};
+
+                http::client()->query_keys(
+                  req,
+                  [keeper = std::move(keeper), megolm_payload, txn_id, this](
+                    const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::net()->warn("failed to query device keys: {} {}",
+                                                     err->matrix_error.error,
+                                                     static_cast<int>(err->status_code));
+                                  // TODO: Mark the event as failed. Communicate with the UI.
+                                  emit messageFailed(QString::fromStdString(txn_id));
+                                  return;
+                          }
+
+                          for (const auto &user : res.device_keys) {
+                                  // Mapping from a device_id with valid identity keys to the
+                                  // generated room_key event used for sharing the megolm session.
+                                  std::map<std::string, std::string> room_key_msgs;
+                                  std::map<std::string, DevicePublicKeys> deviceKeys;
+
+                                  room_key_msgs.clear();
+                                  deviceKeys.clear();
+
+                                  for (const auto &dev : user.second) {
+                                          const auto user_id   = ::UserId(dev.second.user_id);
+                                          const auto device_id = DeviceId(dev.second.device_id);
+
+                                          const auto device_keys = dev.second.keys;
+                                          const auto curveKey    = "curve25519:" + device_id.get();
+                                          const auto edKey       = "ed25519:" + device_id.get();
+
+                                          if ((device_keys.find(curveKey) == device_keys.end()) ||
+                                              (device_keys.find(edKey) == device_keys.end())) {
+                                                  nhlog::net()->debug(
+                                                    "ignoring malformed keys for device {}",
+                                                    device_id.get());
+                                                  continue;
+                                          }
+
+                                          DevicePublicKeys pks;
+                                          pks.ed25519    = device_keys.at(edKey);
+                                          pks.curve25519 = device_keys.at(curveKey);
+
+                                          try {
+                                                  if (!mtx::crypto::verify_identity_signature(
+                                                        json(dev.second), device_id, user_id)) {
+                                                          nhlog::crypto()->warn(
+                                                            "failed to verify identity keys: {}",
+                                                            json(dev.second).dump(2));
+                                                          continue;
+                                                  }
+                                          } catch (const json::exception &e) {
+                                                  nhlog::crypto()->warn(
+                                                    "failed to parse device key json: {}",
+                                                    e.what());
+                                                  continue;
+                                          } catch (const mtx::crypto::olm_exception &e) {
+                                                  nhlog::crypto()->warn(
+                                                    "failed to verify device key json: {}",
+                                                    e.what());
+                                                  continue;
+                                          }
+
+                                          auto room_key = olm::client()
+                                                            ->create_room_key_event(
+                                                              user_id, pks.ed25519, megolm_payload)
+                                                            .dump();
+
+                                          room_key_msgs.emplace(device_id, room_key);
+                                          deviceKeys.emplace(device_id, pks);
+                                  }
+
+                                  std::vector<std::string> valid_devices;
+                                  valid_devices.reserve(room_key_msgs.size());
+                                  for (auto const &d : room_key_msgs) {
+                                          valid_devices.push_back(d.first);
+
+                                          nhlog::net()->info("{}", d.first);
+                                          nhlog::net()->info("  curve25519 {}",
+                                                             deviceKeys.at(d.first).curve25519);
+                                          nhlog::net()->info("  ed25519 {}",
+                                                             deviceKeys.at(d.first).ed25519);
+                                  }
+
+                                  nhlog::net()->info(
+                                    "sending claim request for user {} with {} devices",
+                                    user.first,
+                                    valid_devices.size());
+
+                                  http::client()->claim_keys(
+                                    user.first,
+                                    valid_devices,
+                                    std::bind(&TimelineModel::handleClaimedKeys,
+                                              this,
+                                              keeper,
+                                              room_key_msgs,
+                                              deviceKeys,
+                                              user.first,
+                                              std::placeholders::_1,
+                                              std::placeholders::_2));
+
+                                  // TODO: Wait before sending the next batch of requests.
+                                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
+                          }
+                  });
+
+                // TODO: Let the user know about the errors.
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical(
+                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
+                emit messageFailed(QString::fromStdString(txn_id));
+        } catch (const mtx::crypto::olm_exception &e) {
+                nhlog::crypto()->critical(
+                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
+                emit messageFailed(QString::fromStdString(txn_id));
+        }
+}
+
+void
+TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+                                 const std::map<std::string, std::string> &room_keys,
+                                 const std::map<std::string, DevicePublicKeys> &pks,
+                                 const std::string &user_id,
+                                 const mtx::responses::ClaimKeys &res,
+                                 mtx::http::RequestErr err)
+{
+        if (err) {
+                nhlog::net()->warn("claim keys error: {} {} {}",
+                                   err->matrix_error.error,
+                                   err->parse_error,
+                                   static_cast<int>(err->status_code));
+                return;
+        }
+
+        nhlog::net()->debug("claimed keys for {}", user_id);
+
+        if (res.one_time_keys.size() == 0) {
+                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
+                return;
+        }
+
+        if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
+                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
+                return;
+        }
+
+        auto retrieved_devices = res.one_time_keys.at(user_id);
+
+        // Payload with all the to_device message to be sent.
+        json body;
+        body["messages"][user_id] = json::object();
+
+        for (const auto &rd : retrieved_devices) {
+                const auto device_id = rd.first;
+                nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
+
+                // TODO: Verify signatures
+                auto otk = rd.second.begin()->at("key");
+
+                if (pks.find(device_id) == pks.end()) {
+                        nhlog::net()->critical("couldn't find public key for device: {}",
+                                               device_id);
+                        continue;
+                }
+
+                auto id_key = pks.at(device_id).curve25519;
+                auto s      = olm::client()->create_outbound_session(id_key, otk);
+
+                if (room_keys.find(device_id) == room_keys.end()) {
+                        nhlog::net()->critical("couldn't find m.room_key for device: {}",
+                                               device_id);
+                        continue;
+                }
+
+                auto device_msg = olm::client()->create_olm_encrypted_content(
+                  s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
+
+                try {
+                        cache::client()->saveOlmSession(id_key, std::move(s));
+                } catch (const lmdb::error &e) {
+                        nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
+                } catch (const mtx::crypto::olm_exception &e) {
+                        nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
+                                                  e.what());
+                }
+
+                body["messages"][user_id][device_id] = device_msg;
+        }
+
+        nhlog::net()->info("send_to_device: {}", user_id);
+
+        http::client()->send_to_device(
+          "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to send "
+                                             "send_to_device "
+                                             "message: {}",
+                                             err->matrix_error.error);
+                  }
+
+                  (void)keeper;
+          });
+}
+
+struct SendMessageVisitor
+{
+        SendMessageVisitor(const QString &txn_id, TimelineModel *model)
+          : txn_id_qstr_(txn_id)
+          , model_(model)
+        {}
+
+        template<typename T>
+        void operator()(const mtx::events::Event<T> &)
+        {}
+
+        template<typename T,
+                 std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
+        void operator()(const mtx::events::RoomEvent<T> &msg)
+
+        {
+                if (cache::client()->isRoomEncrypted(model_->room_id_.toStdString())) {
+                        model_->sendEncryptedMessage(txn_id_qstr_.toStdString(),
+                                                     nlohmann::json(msg.content));
+                } else {
+                        QString txn_id_qstr  = txn_id_qstr_;
+                        TimelineModel *model = model_;
+                        http::client()->send_room_message<T, mtx::events::EventType::RoomMessage>(
+                          model->room_id_.toStdString(),
+                          txn_id_qstr.toStdString(),
+                          msg.content,
+                          [txn_id_qstr, model](const mtx::responses::EventId &res,
+                                               mtx::http::RequestErr err) {
+                                  if (err) {
+                                          const int status_code =
+                                            static_cast<int>(err->status_code);
+                                          nhlog::net()->warn("[{}] failed to send message: {} {}",
+                                                             txn_id_qstr.toStdString(),
+                                                             err->matrix_error.error,
+                                                             status_code);
+                                          emit model->messageFailed(txn_id_qstr);
+                                  }
+                                  emit model->messageSent(
+                                    txn_id_qstr, QString::fromStdString(res.event_id.to_string()));
+                          });
+                }
+        }
+
+        QString txn_id_qstr_;
+        TimelineModel *model_;
+};
+
+void
+TimelineModel::processOnePendingMessage()
+{
+        if (isProcessingPending || pending.isEmpty())
+                return;
+
+        isProcessingPending = true;
+
+        QString txn_id_qstr = pending.first();
+
+        auto event = events.value(txn_id_qstr);
+        boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, event);
+}
+
+void
+TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
+{
+        internalAddEvents({event});
+
+        QString txn_id_qstr =
+          boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, event);
+        beginInsertRows(QModelIndex(),
+                        static_cast<int>(this->eventOrder.size()),
+                        static_cast<int>(this->eventOrder.size()));
+        pending.push_back(txn_id_qstr);
+        this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr);
+        endInsertRows();
+        updateLastMessage();
+
+        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
new file mode 100644
index 0000000000000000000000000000000000000000..06c64acf5475aaedd5be7630bae52013b752f9fb
--- /dev/null
+++ b/src/timeline/TimelineModel.h
@@ -0,0 +1,242 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QColor>
+#include <QDate>
+#include <QHash>
+#include <QSet>
+
+#include <mtx/common.hpp>
+#include <mtx/responses.hpp>
+
+#include "Cache.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+
+namespace qml_mtx_events {
+Q_NAMESPACE
+
+enum EventType
+{
+        // Unsupported event
+        Unsupported,
+        /// m.room_key_request
+        KeyRequest,
+        /// m.room.aliases
+        Aliases,
+        /// m.room.avatar
+        Avatar,
+        /// m.room.canonical_alias
+        CanonicalAlias,
+        /// m.room.create
+        Create,
+        /// m.room.encrypted.
+        Encrypted,
+        /// m.room.encryption.
+        Encryption,
+        /// m.room.guest_access
+        GuestAccess,
+        /// m.room.history_visibility
+        HistoryVisibility,
+        /// m.room.join_rules
+        JoinRules,
+        /// m.room.member
+        Member,
+        /// m.room.name
+        Name,
+        /// m.room.power_levels
+        PowerLevels,
+        /// m.room.tombstone
+        Tombstone,
+        /// m.room.topic
+        Topic,
+        /// m.room.redaction
+        Redaction,
+        /// m.room.pinned_events
+        PinnedEvents,
+        // m.sticker
+        Sticker,
+        // m.tag
+        Tag,
+        /// m.room.message
+        AudioMessage,
+        EmoteMessage,
+        FileMessage,
+        ImageMessage,
+        LocationMessage,
+        NoticeMessage,
+        TextMessage,
+        VideoMessage,
+        Redacted,
+        UnknownMessage,
+};
+Q_ENUM_NS(EventType)
+
+enum EventState
+{
+        //! The plaintext message was received by the server.
+        Received,
+        //! At least one of the participants has read the message.
+        Read,
+        //! The client sent the message. Not yet received.
+        Sent,
+        //! When the message is loaded from cache or backfill.
+        Empty,
+        //! When the message failed to send
+        Failed,
+};
+Q_ENUM_NS(EventState)
+}
+
+class StateKeeper
+{
+public:
+        StateKeeper(std::function<void()> &&fn)
+          : fn_(std::move(fn))
+        {}
+
+        ~StateKeeper() { fn_(); }
+
+private:
+        std::function<void()> fn_;
+};
+
+struct DecryptionResult
+{
+        //! The decrypted content as a normal plaintext event.
+        mtx::events::collections::TimelineEvents event;
+        //! Whether or not the decryption was successful.
+        bool isDecrypted = false;
+};
+
+class TimelineViewManager;
+
+class TimelineModel : public QAbstractListModel
+{
+        Q_OBJECT
+        Q_PROPERTY(
+          int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
+
+public:
+        explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0);
+
+        enum Roles
+        {
+                Section,
+                Type,
+                Body,
+                FormattedBody,
+                UserId,
+                UserName,
+                Timestamp,
+                Url,
+                ThumbnailUrl,
+                Filename,
+                Filesize,
+                MimeType,
+                Height,
+                Width,
+                ProportionalHeight,
+                Id,
+                State,
+                IsEncrypted,
+                ReplyTo,
+        };
+
+        QHash<int, QByteArray> roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+        Q_INVOKABLE QColor userColor(QString id, QColor background);
+        Q_INVOKABLE QString displayName(QString id) const;
+        Q_INVOKABLE QString avatarUrl(QString id) const;
+        Q_INVOKABLE QString formatDateSeparator(QDate date) const;
+
+        Q_INVOKABLE QString escapeEmoji(QString str) const;
+        Q_INVOKABLE void viewRawMessage(QString id) const;
+        Q_INVOKABLE void openUserProfile(QString userid) const;
+        Q_INVOKABLE void replyAction(QString id);
+        Q_INVOKABLE void readReceiptsAction(QString id) const;
+        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>
+        void sendMessage(const T &msg);
+
+public slots:
+        void fetchHistory();
+        void setCurrentIndex(int index);
+        int currentIndex() const { return idToIndex(currentId); }
+        void markEventsAsRead(const std::vector<QString> &event_ids);
+
+private slots:
+        // Add old events at the top of the timeline.
+        void addBackwardsEvents(const mtx::responses::Messages &msgs);
+        void processOnePendingMessage();
+        void addPendingMessage(mtx::events::collections::TimelineEvents event);
+
+signals:
+        void oldMessagesRetrieved(const mtx::responses::Messages &res);
+        void messageFailed(QString txn_id);
+        void messageSent(QString txn_id, QString event_id);
+        void currentIndexChanged(int index);
+        void redactionFailed(QString id);
+        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(
+          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const;
+        std::vector<QString> internalAddEvents(
+          const std::vector<mtx::events::collections::TimelineEvents> &timeline);
+        void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content);
+        void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+                               const std::map<std::string, std::string> &room_key,
+                               const std::map<std::string, DevicePublicKeys> &pks,
+                               const std::string &user_id,
+                               const mtx::responses::ClaimKeys &res,
+                               mtx::http::RequestErr err);
+        void updateLastMessage();
+        void readEvent(const std::string &id);
+
+        QHash<QString, mtx::events::collections::TimelineEvents> events;
+        QSet<QString> failed, read;
+        QList<QString> pending;
+        std::vector<QString> eventOrder;
+
+        QString room_id_;
+        QString prev_batch_token_;
+
+        bool isInitialSync        = true;
+        bool paginationInProgress = false;
+        bool isProcessingPending  = false;
+
+        QHash<QString, QColor> userColors;
+        QString currentId;
+
+        TimelineViewManager *manager_;
+
+        friend struct SendMessageVisitor;
+};
+
+template<class T>
+void
+TimelineModel::sendMessage(const T &msg)
+{
+        auto txn_id                       = http::client()->generate_txn_id();
+        mtx::events::RoomEvent<T> msgCopy = {};
+        msgCopy.content                   = msg;
+        msgCopy.type                      = mtx::events::EventType::RoomMessage;
+        msgCopy.event_id                  = txn_id;
+        msgCopy.sender                    = http::client()->user_id().to_string();
+        msgCopy.origin_server_ts          = QDateTime::currentMSecsSinceEpoch();
+
+        emit newMessageToSend(msgCopy);
+}
diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp
deleted file mode 100644
index ed783e9013e392bb6d75b1ea252d78d4d1daa3e2..0000000000000000000000000000000000000000
--- a/src/timeline/TimelineView.cpp
+++ /dev/null
@@ -1,1627 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <boost/variant.hpp>
-
-#include <QApplication>
-#include <QFileInfo>
-#include <QTimer>
-#include <QtConcurrent>
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "Olm.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "ui/FloatingButton.h"
-#include "ui/InfoMessage.h"
-
-#include "timeline/TimelineView.h"
-#include "timeline/widgets/AudioItem.h"
-#include "timeline/widgets/FileItem.h"
-#include "timeline/widgets/ImageItem.h"
-#include "timeline/widgets/VideoItem.h"
-
-using TimelineEvent = mtx::events::collections::TimelineEvents;
-
-//! Maximum number of widgets to keep in the timeline layout.
-constexpr int MAX_RETAINED_WIDGETS = 100;
-constexpr int MIN_SCROLLBAR_HANDLE = 60;
-
-//! Retrieve the timestamp of the event represented by the given widget.
-QDateTime
-getDate(QWidget *widget)
-{
-        auto item = qobject_cast<TimelineItem *>(widget);
-        if (item)
-                return item->descriptionMessage().datetime;
-
-        auto infoMsg = qobject_cast<InfoMessage *>(widget);
-        if (infoMsg)
-                return infoMsg->datetime();
-
-        return QDateTime();
-}
-
-TimelineView::TimelineView(const mtx::responses::Timeline &timeline,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , room_id_{room_id}
-{
-        init();
-        addEvents(timeline);
-}
-
-TimelineView::TimelineView(const QString &room_id, QWidget *parent)
-  : QWidget(parent)
-  , room_id_{room_id}
-{
-        init();
-        getMessages();
-}
-
-void
-TimelineView::sliderRangeChanged(int min, int max)
-{
-        Q_UNUSED(min);
-
-        if (!scroll_area_->verticalScrollBar()->isVisible()) {
-                scroll_area_->verticalScrollBar()->setValue(max);
-                return;
-        }
-
-        // If the scrollbar is close to the bottom and a new message
-        // is added we move the scrollbar.
-        if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) {
-                scroll_area_->verticalScrollBar()->setValue(max);
-                return;
-        }
-
-        int currentHeight = scroll_widget_->size().height();
-        int diff          = currentHeight - oldHeight_;
-        int newPosition   = oldPosition_ + diff;
-
-        // Keep the scroll bar to the bottom if it hasn't been activated yet.
-        if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible())
-                newPosition = max;
-
-        if (lastMessageDirection_ == TimelineDirection::Top)
-                scroll_area_->verticalScrollBar()->setValue(newPosition);
-}
-
-void
-TimelineView::fetchHistory()
-{
-        if (!isScrollbarActivated() && !isTimelineFinished) {
-                if (!isVisible())
-                        return;
-
-                isPaginationInProgress_ = true;
-                getMessages();
-                paginationTimer_->start(2000);
-
-                return;
-        }
-
-        paginationTimer_->stop();
-}
-
-void
-TimelineView::scrollDown()
-{
-        int current = scroll_area_->verticalScrollBar()->value();
-        int max     = scroll_area_->verticalScrollBar()->maximum();
-
-        // The first time we enter the room move the scroll bar to the bottom.
-        if (!isInitialized) {
-                scroll_area_->verticalScrollBar()->setValue(max);
-                isInitialized = true;
-                return;
-        }
-
-        // If the gap is small enough move the scroll bar down. e.g when a new
-        // message appears.
-        if (max - current < SCROLL_BAR_GAP)
-                scroll_area_->verticalScrollBar()->setValue(max);
-}
-
-void
-TimelineView::sliderMoved(int position)
-{
-        if (!scroll_area_->verticalScrollBar()->isVisible())
-                return;
-
-        toggleScrollDownButton();
-
-        // The scrollbar is high enough so we can start retrieving old events.
-        if (position < SCROLL_BAR_GAP) {
-                if (isTimelineFinished)
-                        return;
-
-                // Prevent user from moving up when there is pagination in
-                // progress.
-                if (isPaginationInProgress_)
-                        return;
-
-                isPaginationInProgress_ = true;
-
-                getMessages();
-        }
-}
-
-bool
-TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs)
-{
-        return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start));
-}
-
-void
-TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs)
-{
-        // We've reached the start of the timline and there're no more messages.
-        if (isStartOfTimeline(msgs)) {
-                nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch",
-                                  room_id_.toStdString());
-                isTimelineFinished = true;
-                return;
-        }
-
-        isTimelineFinished = false;
-
-        // Queue incoming messages to be rendered later.
-        topMessages_.insert(topMessages_.end(),
-                            std::make_move_iterator(msgs.chunk.begin()),
-                            std::make_move_iterator(msgs.chunk.end()));
-
-        // The RoomList message preview will be updated only if this
-        // is the first batch of messages received through /messages
-        // i.e there are no other messages currently present.
-        if (!topMessages_.empty() && scroll_layout_->count() == 0)
-                notifyForLastEvent(findFirstViewableEvent(topMessages_));
-
-        if (isVisible()) {
-                renderTopEvents(topMessages_);
-
-                // Free up space for new messages.
-                topMessages_.clear();
-
-                // Send a read receipt for the last event.
-                if (isActiveWindow())
-                        readLastEvent();
-        }
-
-        prev_batch_token_       = QString::fromStdString(msgs.end);
-        isPaginationInProgress_ = false;
-}
-
-QWidget *
-TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
-                                TimelineDirection direction)
-{
-        using namespace mtx::events;
-
-        using AudioEvent  = RoomEvent<msg::Audio>;
-        using EmoteEvent  = RoomEvent<msg::Emote>;
-        using FileEvent   = RoomEvent<msg::File>;
-        using ImageEvent  = RoomEvent<msg::Image>;
-        using NoticeEvent = RoomEvent<msg::Notice>;
-        using TextEvent   = RoomEvent<msg::Text>;
-        using VideoEvent  = RoomEvent<msg::Video>;
-
-        if (boost::get<RedactionEvent<msg::Redaction>>(&event) != nullptr) {
-                auto redaction_event = boost::get<RedactionEvent<msg::Redaction>>(event);
-                const auto event_id  = QString::fromStdString(redaction_event.redacts);
-
-                QTimer::singleShot(0, this, [event_id, this]() {
-                        if (eventIds_.contains(event_id))
-                                removeEvent(event_id);
-                });
-
-                return nullptr;
-        } else if (boost::get<StateEvent<state::Encryption>>(&event) != nullptr) {
-                auto msg      = boost::get<StateEvent<state::Encryption>>(event);
-                auto event_id = QString::fromStdString(msg.event_id);
-
-                if (eventIds_.contains(event_id))
-                        return nullptr;
-
-                auto item = new InfoMessage(tr("Encryption is enabled"), this);
-                item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts));
-                eventIds_[event_id] = item;
-
-                // Force the next message to have avatar by not providing the current username.
-                saveMessageInfo("", msg.origin_server_ts, direction);
-
-                return item;
-        } else if (boost::get<RoomEvent<msg::Audio>>(&event) != nullptr) {
-                auto audio = boost::get<RoomEvent<msg::Audio>>(event);
-                return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
-        } else if (boost::get<RoomEvent<msg::Emote>>(&event) != nullptr) {
-                auto emote = boost::get<RoomEvent<msg::Emote>>(event);
-                return processMessageEvent<EmoteEvent>(emote, direction);
-        } else if (boost::get<RoomEvent<msg::File>>(&event) != nullptr) {
-                auto file = boost::get<RoomEvent<msg::File>>(event);
-                return processMessageEvent<FileEvent, FileItem>(file, direction);
-        } else if (boost::get<RoomEvent<msg::Image>>(&event) != nullptr) {
-                auto image = boost::get<RoomEvent<msg::Image>>(event);
-                return processMessageEvent<ImageEvent, ImageItem>(image, direction);
-        } else if (boost::get<RoomEvent<msg::Notice>>(&event) != nullptr) {
-                auto notice = boost::get<RoomEvent<msg::Notice>>(event);
-                return processMessageEvent<NoticeEvent>(notice, direction);
-        } else if (boost::get<RoomEvent<msg::Text>>(&event) != nullptr) {
-                auto text = boost::get<RoomEvent<msg::Text>>(event);
-                return processMessageEvent<TextEvent>(text, direction);
-        } else if (boost::get<RoomEvent<msg::Video>>(&event) != nullptr) {
-                auto video = boost::get<RoomEvent<msg::Video>>(event);
-                return processMessageEvent<VideoEvent, VideoItem>(video, direction);
-        } else if (boost::get<Sticker>(&event) != nullptr) {
-                return processMessageEvent<Sticker, StickerItem>(boost::get<Sticker>(event),
-                                                                 direction);
-        } else if (boost::get<EncryptedEvent<msg::Encrypted>>(&event) != nullptr) {
-                auto res = parseEncryptedEvent(boost::get<EncryptedEvent<msg::Encrypted>>(event));
-                auto widget = parseMessageEvent(res.event, direction);
-
-                if (widget == nullptr)
-                        return nullptr;
-
-                auto item = qobject_cast<TimelineItem *>(widget);
-
-                if (item && res.isDecrypted)
-                        item->markReceived(true);
-                else if (item && !res.isDecrypted)
-                        item->addKeyRequestAction();
-
-                return widget;
-        }
-
-        return nullptr;
-}
-
-DecryptionResult
-TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
-{
-        MegolmSessionIndex index;
-        index.room_id    = room_id_.toStdString();
-        index.session_id = e.content.session_id;
-        index.sender_key = e.content.sender_key;
-
-        mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
-        dummy.origin_server_ts = e.origin_server_ts;
-        dummy.event_id         = e.event_id;
-        dummy.sender           = e.sender;
-        dummy.content.body =
-          tr("-- Encrypted Event (No keys found for decryption) --",
-             "Placeholder, when the message was not decrypted yet or can't be decrypted")
-            .toStdString();
-
-        try {
-                if (!cache::client()->inboundMegolmSessionExists(index)) {
-                        nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
-                                              index.room_id,
-                                              index.session_id,
-                                              e.sender);
-                        // TODO: request megolm session_id & session_key from the sender.
-                        return {dummy, false};
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
-                dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
-                                        "Placeholder, when the message can't be decrypted, because "
-                                        "the DB access failed when trying to lookup the session.")
-                                       .toStdString();
-                return {dummy, false};
-        }
-
-        std::string msg_str;
-        try {
-                auto session = cache::client()->getInboundMegolmSession(index);
-                auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
-                msg_str      = std::string((char *)res.data.data(), res.data.size());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
-                                      index.room_id,
-                                      index.session_id,
-                                      index.sender_key,
-                                      e.what());
-                dummy.content.body =
-                  tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
-                     "Placeholder, when the message can't be decrypted, because the DB access "
-                     "failed.")
-                    .toStdString();
-                return {dummy, false};
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
-                                          index.room_id,
-                                          index.session_id,
-                                          index.sender_key,
-                                          e.what());
-                dummy.content.body =
-                  tr("-- Decryption Error (%1) --",
-                     "Placeholder, when the message can't be decrypted. In this case, the Olm "
-                     "decrytion returned an error, which is passed ad %1")
-                    .arg(e.what())
-                    .toStdString();
-                return {dummy, false};
-        }
-
-        // Add missing fields for the event.
-        json body                = json::parse(msg_str);
-        body["event_id"]         = e.event_id;
-        body["sender"]           = e.sender;
-        body["origin_server_ts"] = e.origin_server_ts;
-        body["unsigned"]         = e.unsigned_data;
-
-        nhlog::crypto()->debug("decrypted event: {}", e.event_id);
-
-        json event_array = json::array();
-        event_array.push_back(body);
-
-        std::vector<TimelineEvent> events;
-        mtx::responses::utils::parse_timeline_events(event_array, events);
-
-        if (events.size() == 1)
-                return {events.at(0), true};
-
-        dummy.content.body =
-          tr("-- Encrypted Event (Unknown event type) --",
-             "Placeholder, when the message was decrypted, but we couldn't parse it, because "
-             "Nheko/mtxclient don't support that event type yet")
-            .toStdString();
-        return {dummy, false};
-}
-
-void
-TimelineView::displayReadReceipts(std::vector<TimelineEvent> events)
-{
-        QtConcurrent::run(
-          [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() {
-                  std::vector<QString> event_ids;
-
-                  for (const auto &e : events) {
-                          if (utils::event_sender(e) == local_user)
-                                  event_ids.emplace_back(
-                                    QString::fromStdString(utils::event_id(e)));
-                  }
-
-                  auto readEvents =
-                    cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString());
-
-                  if (!readEvents.empty())
-                          emit markReadEvents(readEvents);
-          });
-}
-
-void
-TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
-{
-        int counter = 0;
-
-        for (const auto &event : events) {
-                QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom);
-
-                if (item != nullptr) {
-                        addTimelineItem(item, TimelineDirection::Bottom);
-                        counter++;
-
-                        // Prevent blocking of the event-loop
-                        // by calling processEvents every 10 items we render.
-                        if (counter % 4 == 0)
-                                QApplication::processEvents();
-                }
-        }
-
-        lastMessageDirection_ = TimelineDirection::Bottom;
-
-        displayReadReceipts(events);
-
-        QApplication::processEvents();
-}
-
-void
-TimelineView::renderTopEvents(const std::vector<TimelineEvent> &events)
-{
-        std::vector<QWidget *> items;
-
-        // Reset the sender of the first message in the timeline
-        // cause we're about to insert a new one.
-        firstSender_.clear();
-        firstMsgTimestamp_ = QDateTime();
-
-        // Parse in reverse order to determine where we should not show sender's name.
-        for (auto it = events.rbegin(); it != events.rend(); ++it) {
-                auto item = parseMessageEvent(*it, TimelineDirection::Top);
-
-                if (item != nullptr)
-                        items.push_back(item);
-        }
-
-        // Reverse again to render them.
-        std::reverse(items.begin(), items.end());
-
-        oldPosition_ = scroll_area_->verticalScrollBar()->value();
-        oldHeight_   = scroll_widget_->size().height();
-
-        for (const auto &item : items)
-                addTimelineItem(item, TimelineDirection::Top);
-
-        lastMessageDirection_ = TimelineDirection::Top;
-
-        QApplication::processEvents();
-
-        displayReadReceipts(events);
-
-        // If this batch is the first being rendered (i.e the first and the last
-        // events originate from this batch), set the last sender.
-        if (lastSender_.isEmpty() && !items.empty()) {
-                for (const auto &w : items) {
-                        auto timelineItem = qobject_cast<TimelineItem *>(w);
-                        if (timelineItem) {
-                                saveLastMessageInfo(timelineItem->descriptionMessage().userid,
-                                                    timelineItem->descriptionMessage().datetime);
-                                break;
-                        }
-                }
-        }
-}
-
-void
-TimelineView::addEvents(const mtx::responses::Timeline &timeline)
-{
-        if (isInitialSync) {
-                prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
-                isInitialSync     = false;
-        }
-
-        bottomMessages_.insert(bottomMessages_.end(),
-                               std::make_move_iterator(timeline.events.begin()),
-                               std::make_move_iterator(timeline.events.end()));
-
-        if (!bottomMessages_.empty())
-                notifyForLastEvent(findLastViewableEvent(bottomMessages_));
-
-        // If the current timeline is open and there are messages to be rendered.
-        if (isVisible() && !bottomMessages_.empty()) {
-                renderBottomEvents(bottomMessages_);
-
-                // Free up space for new messages.
-                bottomMessages_.clear();
-
-                // Send a read receipt for the last event.
-                if (isActiveWindow())
-                        readLastEvent();
-        }
-}
-
-void
-TimelineView::init()
-{
-        local_user_ = utils::localUser();
-
-        QIcon icon;
-        icon.addFile(":/icons/icons/ui/angle-arrow-down.png");
-        scrollDownBtn_ = new FloatingButton(icon, this);
-        scrollDownBtn_->hide();
-
-        connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() {
-                const int max = scroll_area_->verticalScrollBar()->maximum();
-                scroll_area_->verticalScrollBar()->setValue(max);
-        });
-        top_layout_ = new QVBoxLayout(this);
-        top_layout_->setSpacing(0);
-        top_layout_->setMargin(0);
-
-        scroll_area_ = new QScrollArea(this);
-        scroll_area_->setWidgetResizable(true);
-        scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-        scroll_widget_ = new QWidget(this);
-        scroll_widget_->setObjectName("scroll_widget");
-
-        // Height of the typing display.
-        QFont f;
-        f.setPointSizeF(f.pointSizeF() * 0.9);
-        const int bottomMargin = QFontMetrics(f).height() + 6;
-
-        scroll_layout_ = new QVBoxLayout(scroll_widget_);
-        scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
-        scroll_layout_->setSpacing(0);
-        scroll_layout_->setObjectName("timelinescrollarea");
-
-        scroll_area_->setWidget(scroll_widget_);
-        scroll_area_->setAlignment(Qt::AlignBottom);
-
-        top_layout_->addWidget(scroll_area_);
-
-        setLayout(top_layout_);
-
-        paginationTimer_ = new QTimer(this);
-        connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
-
-        connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents);
-
-        connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
-        connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage);
-
-        connect(
-          this, &TimelineView::markReadEvents, this, [this](const std::vector<QString> &event_ids) {
-                  for (const auto &event : event_ids) {
-                          if (eventIds_.contains(event)) {
-                                  auto widget = eventIds_[event];
-                                  if (!widget)
-                                          return;
-
-                                  auto item = qobject_cast<TimelineItem *>(widget);
-                                  if (!item)
-                                          return;
-
-                                  item->markRead();
-                          }
-                  }
-          });
-
-        connect(scroll_area_->verticalScrollBar(),
-                SIGNAL(valueChanged(int)),
-                this,
-                SLOT(sliderMoved(int)));
-        connect(scroll_area_->verticalScrollBar(),
-                SIGNAL(rangeChanged(int, int)),
-                this,
-                SLOT(sliderRangeChanged(int, int)));
-}
-
-void
-TimelineView::getMessages()
-{
-        mtx::http::MessagesOpts opts;
-        opts.room_id = room_id_.toStdString();
-        opts.from    = prev_batch_token_.toStdString();
-
-        http::client()->messages(
-          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->error("failed to call /messages ({}): {} - {}",
-                                              opts.room_id,
-                                              mtx::errors::to_string(err->matrix_error.errcode),
-                                              err->matrix_error.error);
-                          return;
-                  }
-
-                  emit messagesRetrieved(std::move(res));
-          });
-}
-
-void
-TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
-{
-        if (direction == TimelineDirection::Bottom)
-                lastSender_ = user_id;
-        else
-                firstSender_ = user_id;
-}
-
-bool
-TimelineView::isSenderRendered(const QString &user_id,
-                               uint64_t origin_server_ts,
-                               TimelineDirection direction)
-{
-        if (direction == TimelineDirection::Bottom) {
-                return (lastSender_ != user_id) ||
-                       isDateDifference(lastMsgTimestamp_,
-                                        QDateTime::fromMSecsSinceEpoch(origin_server_ts));
-        } else {
-                return (firstSender_ != user_id) ||
-                       isDateDifference(firstMsgTimestamp_,
-                                        QDateTime::fromMSecsSinceEpoch(origin_server_ts));
-        }
-}
-
-void
-TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction)
-{
-        const auto newDate = getDate(item);
-
-        if (direction == TimelineDirection::Bottom) {
-                QWidget *lastItem    = nullptr;
-                int lastItemPosition = 0;
-
-                if (scroll_layout_->count() > 0) {
-                        lastItemPosition = scroll_layout_->count() - 1;
-                        lastItem         = scroll_layout_->itemAt(lastItemPosition)->widget();
-                }
-
-                if (lastItem) {
-                        const auto oldDate = getDate(lastItem);
-
-                        if (oldDate.daysTo(newDate) != 0) {
-                                auto separator = new DateSeparator(newDate, this);
-
-                                if (separator)
-                                        pushTimelineItem(separator, direction);
-                        }
-                }
-
-                pushTimelineItem(item, direction);
-        } else {
-                if (scroll_layout_->count() > 0) {
-                        const auto firstItem = scroll_layout_->itemAt(0)->widget();
-
-                        if (firstItem) {
-                                const auto oldDate = getDate(firstItem);
-
-                                if (newDate.daysTo(oldDate) != 0) {
-                                        auto separator = new DateSeparator(oldDate);
-
-                                        if (separator)
-                                                pushTimelineItem(separator, direction);
-                                }
-                        }
-                }
-
-                pushTimelineItem(item, direction);
-        }
-}
-
-void
-TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id)
-{
-        nhlog::ui()->debug("[{}] message was received by the server", txn_id);
-        if (!pending_msgs_.isEmpty() &&
-            pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
-                auto msg     = pending_msgs_.dequeue();
-                msg.event_id = event_id;
-
-                if (msg.widget) {
-                        msg.widget->setEventId(event_id);
-                        eventIds_[event_id] = msg.widget;
-
-                        // If the response comes after we have received the event from sync
-                        // we've already marked the widget as received.
-                        if (!msg.widget->isReceived()) {
-                                msg.widget->markReceived(msg.is_encrypted);
-                                cache::client()->addPendingReceipt(room_id_, event_id);
-                                pending_sent_msgs_.append(msg);
-                        }
-                } else {
-                        nhlog::ui()->warn("[{}] received message response for invalid widget",
-                                          txn_id);
-                }
-        }
-
-        sendNextPendingMessage();
-}
-
-void
-TimelineView::addUserMessage(mtx::events::MessageType ty,
-                             const QString &body,
-                             const RelatedInfo &related = RelatedInfo())
-{
-        auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_);
-
-        QString full_body;
-        if (related.related_event.empty()) {
-                full_body = body;
-        } else {
-                full_body = utils::getFormattedQuoteBody(related, body);
-        }
-        TimelineItem *view_item =
-          new TimelineItem(ty, local_user_, full_body, with_sender, room_id_, scroll_widget_);
-
-        PendingMessage message;
-        message.ty      = ty;
-        message.txn_id  = http::client()->generate_txn_id();
-        message.body    = body;
-        message.related = related;
-        message.widget  = view_item;
-
-        try {
-                message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to check encryption status of room {}", e.what());
-                view_item->deleteLater();
-
-                // TODO: Send a notification to the user.
-
-                return;
-        }
-
-        addTimelineItem(view_item);
-
-        lastMessageDirection_ = TimelineDirection::Bottom;
-
-        saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
-        handleNewUserMessage(message);
-}
-
-void
-TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
-{
-        addUserMessage(ty, body, RelatedInfo());
-}
-
-void
-TimelineView::handleNewUserMessage(PendingMessage msg)
-{
-        pending_msgs_.enqueue(msg);
-        if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty())
-                sendNextPendingMessage();
-}
-
-void
-TimelineView::sendNextPendingMessage()
-{
-        if (pending_msgs_.size() == 0)
-                return;
-
-        using namespace mtx::events;
-
-        PendingMessage &m = pending_msgs_.head();
-
-        nhlog::ui()->debug("[{}] sending next queued message", m.txn_id);
-
-        if (m.widget)
-                m.widget->markSent();
-
-        if (m.is_encrypted) {
-                nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id);
-                prepareEncryptedMessage(std::move(m));
-                return;
-        }
-
-        switch (m.ty) {
-        case mtx::events::MessageType::Audio: {
-                http::client()->send_room_message<msg::Audio, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Audio>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Image: {
-                http::client()->send_room_message<msg::Image, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Image>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Video: {
-                http::client()->send_room_message<msg::Video, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Video>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::File: {
-                http::client()->send_room_message<msg::File, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::File>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Text: {
-                http::client()->send_room_message<msg::Text, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Text>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Emote: {
-                http::client()->send_room_message<msg::Emote, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Emote>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-                break;
-        }
-        default:
-                nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString());
-                break;
-        }
-}
-
-void
-TimelineView::notifyForLastEvent()
-{
-        if (scroll_layout_->count() == 0) {
-                nhlog::ui()->error("notifyForLastEvent called with empty timeline");
-                return;
-        }
-
-        auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1);
-
-        if (!lastItem)
-                return;
-
-        auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget());
-
-        if (lastTimelineItem)
-                emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
-        else
-                nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString());
-}
-
-void
-TimelineView::notifyForLastEvent(const TimelineEvent &event)
-{
-        auto descInfo = utils::getMessageDescription(event, local_user_, room_id_);
-
-        if (!descInfo.timestamp.isEmpty())
-                emit updateLastTimelineMessage(room_id_, descInfo);
-}
-
-bool
-TimelineView::isPendingMessage(const std::string &txn_id,
-                               const QString &sender,
-                               const QString &local_userid)
-{
-        if (sender != local_userid)
-                return false;
-
-        auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; };
-
-        return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) ||
-               std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid);
-}
-
-void
-TimelineView::removePendingMessage(const std::string &txn_id)
-{
-        if (txn_id.empty())
-                return;
-
-        for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
-                if (it->txn_id == txn_id) {
-                        int index = std::distance(pending_sent_msgs_.begin(), it);
-                        pending_sent_msgs_.removeAt(index);
-
-                        if (pending_sent_msgs_.isEmpty())
-                                sendNextPendingMessage();
-
-                        nhlog::ui()->debug("[{}] removed message with sync", txn_id);
-                }
-        }
-        for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
-                if (it->txn_id == txn_id) {
-                        if (it->widget) {
-                                it->widget->markReceived(it->is_encrypted);
-
-                                // TODO: update when a solution for encrypted messages is available.
-                                if (!it->is_encrypted)
-                                        cache::client()->addPendingReceipt(room_id_, it->event_id);
-                        }
-
-                        nhlog::ui()->debug("[{}] received sync before message response", txn_id);
-                        return;
-                }
-        }
-}
-
-void
-TimelineView::handleFailedMessage(const std::string &txn_id)
-{
-        Q_UNUSED(txn_id);
-        // Note: We do this even if the message has already been echoed.
-        QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage()));
-}
-
-void
-TimelineView::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-TimelineView::readLastEvent() const
-{
-        if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled())
-                return;
-
-        const auto eventId = getLastEventId();
-
-        if (!eventId.isEmpty())
-                http::client()->read_event(room_id_.toStdString(),
-                                           eventId.toStdString(),
-                                           [this, eventId](mtx::http::RequestErr err) {
-                                                   if (err) {
-                                                           nhlog::net()->warn(
-                                                             "failed to read event ({}, {})",
-                                                             room_id_.toStdString(),
-                                                             eventId.toStdString());
-                                                   }
-                                           });
-}
-
-QString
-TimelineView::getLastEventId() const
-{
-        auto index = scroll_layout_->count();
-
-        // Search backwards for the first event that has a valid event id.
-        while (index > 0) {
-                --index;
-
-                auto lastItem          = scroll_layout_->itemAt(index);
-                auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget());
-
-                if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty())
-                        return lastTimelineItem->eventId();
-        }
-
-        return QString("");
-}
-
-void
-TimelineView::showEvent(QShowEvent *event)
-{
-        if (!topMessages_.empty()) {
-                renderTopEvents(topMessages_);
-                topMessages_.clear();
-        }
-
-        if (!bottomMessages_.empty()) {
-                renderBottomEvents(bottomMessages_);
-                bottomMessages_.clear();
-                scrollDown();
-        }
-
-        toggleScrollDownButton();
-
-        readLastEvent();
-
-        QWidget::showEvent(event);
-}
-
-void
-TimelineView::hideEvent(QHideEvent *event)
-{
-        const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height();
-        const auto widgetsNum   = scroll_layout_->count();
-
-        // Remove widgets from the timeline to reduce the memory footprint.
-        if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS)
-                clearTimeline();
-
-        QWidget::hideEvent(event);
-}
-
-bool
-TimelineView::event(QEvent *event)
-{
-        if (event->type() == QEvent::WindowActivate)
-                readLastEvent();
-
-        return QWidget::event(event);
-}
-
-void
-TimelineView::clearTimeline()
-{
-        // Delete all widgets.
-        QLayoutItem *item;
-        while ((item = scroll_layout_->takeAt(0)) != nullptr) {
-                delete item->widget();
-                delete item;
-        }
-
-        // The next call to /messages will be without a prev token.
-        prev_batch_token_.clear();
-        eventIds_.clear();
-
-        // Clear queues with pending messages to be rendered.
-        bottomMessages_.clear();
-        topMessages_.clear();
-
-        firstSender_.clear();
-        lastSender_.clear();
-}
-
-void
-TimelineView::toggleScrollDownButton()
-{
-        const int maxScroll     = scroll_area_->verticalScrollBar()->maximum();
-        const int currentScroll = scroll_area_->verticalScrollBar()->value();
-
-        if (maxScroll - currentScroll > SCROLL_BAR_GAP) {
-                scrollDownBtn_->show();
-                scrollDownBtn_->raise();
-        } else {
-                scrollDownBtn_->hide();
-        }
-}
-
-void
-TimelineView::removeEvent(const QString &event_id)
-{
-        if (!eventIds_.contains(event_id)) {
-                nhlog::ui()->warn("cannot remove widget with unknown event_id: {}",
-                                  event_id.toStdString());
-                return;
-        }
-
-        auto removedItem = eventIds_[event_id];
-
-        // Find the next and the previous widgets in the timeline
-        auto prevWidget = relativeWidget(removedItem, -1);
-        auto nextWidget = relativeWidget(removedItem, 1);
-
-        // See if they are timeline items
-        auto prevItem = qobject_cast<TimelineItem *>(prevWidget);
-        auto nextItem = qobject_cast<TimelineItem *>(nextWidget);
-
-        // ... or a date separator
-        auto prevLabel = qobject_cast<DateSeparator *>(prevWidget);
-
-        // If it's a TimelineItem add an avatar.
-        if (prevItem) {
-                prevItem->addAvatar();
-        }
-
-        if (nextItem) {
-                nextItem->addAvatar();
-        } else if (prevLabel) {
-                // If there's no chat message after this, and we have a label before us, delete the
-                // label.
-                prevLabel->deleteLater();
-        }
-
-        // If we deleted the last item in the timeline...
-        if (!nextItem && prevItem)
-                saveLastMessageInfo(prevItem->descriptionMessage().userid,
-                                    prevItem->descriptionMessage().datetime);
-
-        // If we deleted the first item in the timeline...
-        if (!prevItem && nextItem)
-                saveFirstMessageInfo(nextItem->descriptionMessage().userid,
-                                     nextItem->descriptionMessage().datetime);
-
-        // If we deleted the only item in the timeline...
-        if (!prevItem && !nextItem) {
-                firstSender_.clear();
-                firstMsgTimestamp_ = QDateTime();
-                lastSender_.clear();
-                lastMsgTimestamp_ = QDateTime();
-        }
-
-        // Finally remove the event.
-        removedItem->deleteLater();
-        eventIds_.remove(event_id);
-
-        // Update the room list with a view of the last message after
-        // all events have been processed.
-        QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); });
-}
-
-QWidget *
-TimelineView::relativeWidget(QWidget *item, int dt) const
-{
-        int pos = scroll_layout_->indexOf(item);
-
-        if (pos == -1)
-                return nullptr;
-
-        pos = pos + dt;
-
-        bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1);
-
-        return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget();
-}
-
-TimelineEvent
-TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events)
-{
-        auto it = std::find_if(events.begin(), events.end(), [](const auto &event) {
-                return mtx::events::EventType::RoomMessage == utils::event_type(event);
-        });
-
-        return (it == std::end(events)) ? events.front() : *it;
-}
-
-TimelineEvent
-TimelineView::findLastViewableEvent(const std::vector<TimelineEvent> &events)
-{
-        auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) {
-                return (mtx::events::EventType::RoomMessage == utils::event_type(event)) ||
-                       (mtx::events::EventType::RoomEncrypted == utils::event_type(event));
-        });
-
-        return (it == std::rend(events)) ? events.back() : *it;
-}
-
-void
-TimelineView::saveMessageInfo(const QString &sender,
-                              uint64_t origin_server_ts,
-                              TimelineDirection direction)
-{
-        updateLastSender(sender, direction);
-
-        if (direction == TimelineDirection::Bottom)
-                lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts);
-        else
-                firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts);
-}
-
-bool
-TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const
-{
-        // Check if the dates are in a different day.
-        if (std::abs(first.daysTo(second)) != 0)
-                return true;
-
-        const uint64_t diffInSeconds   = std::abs(first.msecsTo(second)) / 1000;
-        constexpr uint64_t fifteenMins = 15 * 60;
-
-        return diffInSeconds > fifteenMins;
-}
-
-void
-TimelineView::sendRoomMessageHandler(const std::string &txn_id,
-                                     const mtx::responses::EventId &res,
-                                     mtx::http::RequestErr err)
-{
-        if (err) {
-                const int status_code = static_cast<int>(err->status_code);
-                nhlog::net()->warn("[{}] failed to send message: {} {}",
-                                   txn_id,
-                                   err->matrix_error.error,
-                                   status_code);
-                emit messageFailed(txn_id);
-                return;
-        }
-
-        emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string()));
-}
-
-template<>
-mtx::events::msg::Audio
-toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m)
-{
-        mtx::events::msg::Audio audio;
-        audio.info.mimetype = m.mime.toStdString();
-        audio.info.size     = m.media_size;
-        audio.body          = m.filename.toStdString();
-        audio.url           = m.body.toStdString();
-        return audio;
-}
-
-template<>
-mtx::events::msg::Image
-toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m)
-{
-        mtx::events::msg::Image image;
-        image.info.mimetype = m.mime.toStdString();
-        image.info.size     = m.media_size;
-        image.body          = m.filename.toStdString();
-        image.url           = m.body.toStdString();
-        image.info.h        = m.dimensions.height();
-        image.info.w        = m.dimensions.width();
-        return image;
-}
-
-template<>
-mtx::events::msg::Video
-toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m)
-{
-        mtx::events::msg::Video video;
-        video.info.mimetype = m.mime.toStdString();
-        video.info.size     = m.media_size;
-        video.body          = m.filename.toStdString();
-        video.url           = m.body.toStdString();
-        return video;
-}
-
-template<>
-mtx::events::msg::Emote
-toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m)
-{
-        auto html = utils::markdownToHtml(m.body);
-
-        mtx::events::msg::Emote emote;
-        emote.body = m.body.trimmed().toStdString();
-
-        if (html != m.body.trimmed().toHtmlEscaped())
-                emote.formatted_body = html.toStdString();
-
-        return emote;
-}
-
-template<>
-mtx::events::msg::File
-toRoomMessage<mtx::events::msg::File>(const PendingMessage &m)
-{
-        mtx::events::msg::File file;
-        file.info.mimetype = m.mime.toStdString();
-        file.info.size     = m.media_size;
-        file.body          = m.filename.toStdString();
-        file.url           = m.body.toStdString();
-        return file;
-}
-
-template<>
-mtx::events::msg::Text
-toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m)
-{
-        auto html = utils::markdownToHtml(m.body);
-
-        mtx::events::msg::Text text;
-
-        text.body = m.body.trimmed().toStdString();
-
-        if (html != m.body.trimmed().toHtmlEscaped()) {
-                if (!m.related.quoted_body.isEmpty()) {
-                        text.formatted_body =
-                          utils::getFormattedQuoteBody(m.related, html).toStdString();
-                } else {
-                        text.formatted_body = html.toStdString();
-                }
-        }
-
-        if (!m.related.related_event.empty()) {
-                text.relates_to.in_reply_to.event_id = m.related.related_event;
-        }
-
-        return text;
-}
-
-void
-TimelineView::prepareEncryptedMessage(const PendingMessage &msg)
-{
-        const auto room_id = room_id_.toStdString();
-
-        using namespace mtx::events;
-        using namespace mtx::identifiers;
-
-        json content;
-
-        // Serialize the message to the plaintext that will be encrypted.
-        switch (msg.ty) {
-        case MessageType::Audio: {
-                content = json(toRoomMessage<msg::Audio>(msg));
-                break;
-        }
-        case MessageType::Emote: {
-                content = json(toRoomMessage<msg::Emote>(msg));
-                break;
-        }
-        case MessageType::File: {
-                content = json(toRoomMessage<msg::File>(msg));
-                break;
-        }
-        case MessageType::Image: {
-                content = json(toRoomMessage<msg::Image>(msg));
-                break;
-        }
-        case MessageType::Text: {
-                content = json(toRoomMessage<msg::Text>(msg));
-                break;
-        }
-        case MessageType::Video: {
-                content = json(toRoomMessage<msg::Video>(msg));
-                break;
-        }
-        default:
-                break;
-        }
-
-        json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
-
-        try {
-                // Check if we have already an outbound megolm session then we can use.
-                if (cache::client()->outboundMegolmSessionExists(room_id)) {
-                        auto data = olm::encrypt_group_message(
-                          room_id, http::client()->device_id(), doc.dump());
-
-                        http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
-                          room_id,
-                          msg.txn_id,
-                          data,
-                          std::bind(&TimelineView::sendRoomMessageHandler,
-                                    this,
-                                    msg.txn_id,
-                                    std::placeholders::_1,
-                                    std::placeholders::_2));
-                        return;
-                }
-
-                nhlog::ui()->debug("creating new outbound megolm session");
-
-                // Create a new outbound megolm session.
-                auto outbound_session  = olm::client()->init_outbound_group_session();
-                const auto session_id  = mtx::crypto::session_id(outbound_session.get());
-                const auto session_key = mtx::crypto::session_key(outbound_session.get());
-
-                // TODO: needs to be moved in the lib.
-                auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
-                                           {"room_id", room_id},
-                                           {"session_id", session_id},
-                                           {"session_key", session_key}};
-
-                // Saving the new megolm session.
-                // TODO: Maybe it's too early to save.
-                OutboundGroupSessionData session_data;
-                session_data.session_id    = session_id;
-                session_data.session_key   = session_key;
-                session_data.message_index = 0; // TODO Update me
-                cache::client()->saveOutboundMegolmSession(
-                  room_id, session_data, std::move(outbound_session));
-
-                const auto members = cache::client()->roomMembers(room_id);
-                nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
-
-                auto keeper = std::make_shared<StateKeeper>(
-                  [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() {
-                          try {
-                                  auto data = olm::encrypt_group_message(
-                                    room_id, http::client()->device_id(), doc.dump());
-
-                                  http::client()
-                                    ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
-                                      room_id,
-                                      txn_id,
-                                      data,
-                                      std::bind(&TimelineView::sendRoomMessageHandler,
-                                                this,
-                                                txn_id,
-                                                std::placeholders::_1,
-                                                std::placeholders::_2));
-
-                          } catch (const lmdb::error &e) {
-                                  nhlog::db()->critical(
-                                    "failed to save megolm outbound session: {}", e.what());
-                          }
-                  });
-
-                mtx::requests::QueryKeys req;
-                for (const auto &member : members)
-                        req.device_keys[member] = {};
-
-                http::client()->query_keys(
-                  req,
-                  [keeper = std::move(keeper), megolm_payload, this](
-                    const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::net()->warn("failed to query device keys: {} {}",
-                                                     err->matrix_error.error,
-                                                     static_cast<int>(err->status_code));
-                                  // TODO: Mark the event as failed. Communicate with the UI.
-                                  return;
-                          }
-
-                          for (const auto &user : res.device_keys) {
-                                  // Mapping from a device_id with valid identity keys to the
-                                  // generated room_key event used for sharing the megolm session.
-                                  std::map<std::string, std::string> room_key_msgs;
-                                  std::map<std::string, DevicePublicKeys> deviceKeys;
-
-                                  room_key_msgs.clear();
-                                  deviceKeys.clear();
-
-                                  for (const auto &dev : user.second) {
-                                          const auto user_id   = UserId(dev.second.user_id);
-                                          const auto device_id = DeviceId(dev.second.device_id);
-
-                                          const auto device_keys = dev.second.keys;
-                                          const auto curveKey    = "curve25519:" + device_id.get();
-                                          const auto edKey       = "ed25519:" + device_id.get();
-
-                                          if ((device_keys.find(curveKey) == device_keys.end()) ||
-                                              (device_keys.find(edKey) == device_keys.end())) {
-                                                  nhlog::net()->debug(
-                                                    "ignoring malformed keys for device {}",
-                                                    device_id.get());
-                                                  continue;
-                                          }
-
-                                          DevicePublicKeys pks;
-                                          pks.ed25519    = device_keys.at(edKey);
-                                          pks.curve25519 = device_keys.at(curveKey);
-
-                                          try {
-                                                  if (!mtx::crypto::verify_identity_signature(
-                                                        json(dev.second), device_id, user_id)) {
-                                                          nhlog::crypto()->warn(
-                                                            "failed to verify identity keys: {}",
-                                                            json(dev.second).dump(2));
-                                                          continue;
-                                                  }
-                                          } catch (const json::exception &e) {
-                                                  nhlog::crypto()->warn(
-                                                    "failed to parse device key json: {}",
-                                                    e.what());
-                                                  continue;
-                                          } catch (const mtx::crypto::olm_exception &e) {
-                                                  nhlog::crypto()->warn(
-                                                    "failed to verify device key json: {}",
-                                                    e.what());
-                                                  continue;
-                                          }
-
-                                          auto room_key = olm::client()
-                                                            ->create_room_key_event(
-                                                              user_id, pks.ed25519, megolm_payload)
-                                                            .dump();
-
-                                          room_key_msgs.emplace(device_id, room_key);
-                                          deviceKeys.emplace(device_id, pks);
-                                  }
-
-                                  std::vector<std::string> valid_devices;
-                                  valid_devices.reserve(room_key_msgs.size());
-                                  for (auto const &d : room_key_msgs) {
-                                          valid_devices.push_back(d.first);
-
-                                          nhlog::net()->info("{}", d.first);
-                                          nhlog::net()->info("  curve25519 {}",
-                                                             deviceKeys.at(d.first).curve25519);
-                                          nhlog::net()->info("  ed25519 {}",
-                                                             deviceKeys.at(d.first).ed25519);
-                                  }
-
-                                  nhlog::net()->info(
-                                    "sending claim request for user {} with {} devices",
-                                    user.first,
-                                    valid_devices.size());
-
-                                  http::client()->claim_keys(
-                                    user.first,
-                                    valid_devices,
-                                    std::bind(&TimelineView::handleClaimedKeys,
-                                              this,
-                                              keeper,
-                                              room_key_msgs,
-                                              deviceKeys,
-                                              user.first,
-                                              std::placeholders::_1,
-                                              std::placeholders::_2));
-
-                                  // TODO: Wait before sending the next batch of requests.
-                                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
-                          }
-                  });
-
-                // TODO: Let the user know about the errors.
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical(
-                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical(
-                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
-        }
-}
-
-void
-TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
-                                const std::map<std::string, std::string> &room_keys,
-                                const std::map<std::string, DevicePublicKeys> &pks,
-                                const std::string &user_id,
-                                const mtx::responses::ClaimKeys &res,
-                                mtx::http::RequestErr err)
-{
-        if (err) {
-                nhlog::net()->warn("claim keys error: {} {} {}",
-                                   err->matrix_error.error,
-                                   err->parse_error,
-                                   static_cast<int>(err->status_code));
-                return;
-        }
-
-        nhlog::net()->debug("claimed keys for {}", user_id);
-
-        if (res.one_time_keys.size() == 0) {
-                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
-                return;
-        }
-
-        if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
-                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
-                return;
-        }
-
-        auto retrieved_devices = res.one_time_keys.at(user_id);
-
-        // Payload with all the to_device message to be sent.
-        json body;
-        body["messages"][user_id] = json::object();
-
-        for (const auto &rd : retrieved_devices) {
-                const auto device_id = rd.first;
-                nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
-
-                // TODO: Verify signatures
-                auto otk = rd.second.begin()->at("key");
-
-                if (pks.find(device_id) == pks.end()) {
-                        nhlog::net()->critical("couldn't find public key for device: {}",
-                                               device_id);
-                        continue;
-                }
-
-                auto id_key = pks.at(device_id).curve25519;
-                auto s      = olm::client()->create_outbound_session(id_key, otk);
-
-                if (room_keys.find(device_id) == room_keys.end()) {
-                        nhlog::net()->critical("couldn't find m.room_key for device: {}",
-                                               device_id);
-                        continue;
-                }
-
-                auto device_msg = olm::client()->create_olm_encrypted_content(
-                  s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
-
-                try {
-                        cache::client()->saveOlmSession(id_key, std::move(s));
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
-                } catch (const mtx::crypto::olm_exception &e) {
-                        nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
-                                                  e.what());
-                }
-
-                body["messages"][user_id][device_id] = device_msg;
-        }
-
-        nhlog::net()->info("send_to_device: {}", user_id);
-
-        http::client()->send_to_device(
-          "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to send "
-                                             "send_to_device "
-                                             "message: {}",
-                                             err->matrix_error.error);
-                  }
-
-                  (void)keeper;
-          });
-}
diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h
deleted file mode 100644
index 35796efd154a1ce5970d501082df1c5114de3d4c..0000000000000000000000000000000000000000
--- a/src/timeline/TimelineView.h
+++ /dev/null
@@ -1,449 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QApplication>
-#include <QLayout>
-#include <QList>
-#include <QQueue>
-#include <QScrollArea>
-#include <QScrollBar>
-#include <QStyle>
-#include <QStyleOption>
-#include <QTimer>
-
-#include <mtx/events.hpp>
-#include <mtx/responses/messages.hpp>
-
-#include "../Utils.h"
-#include "MatrixClient.h"
-#include "timeline/TimelineItem.h"
-
-class StateKeeper
-{
-public:
-        StateKeeper(std::function<void()> &&fn)
-          : fn_(std::move(fn))
-        {}
-
-        ~StateKeeper() { fn_(); }
-
-private:
-        std::function<void()> fn_;
-};
-
-struct DecryptionResult
-{
-        //! The decrypted content as a normal plaintext event.
-        utils::TimelineEvent event;
-        //! Whether or not the decryption was successful.
-        bool isDecrypted = false;
-};
-
-class FloatingButton;
-struct DescInfo;
-
-// Contains info about a message shown in the history view
-// but not yet confirmed by the homeserver through sync.
-struct PendingMessage
-{
-        mtx::events::MessageType ty;
-        std::string txn_id;
-        RelatedInfo related;
-        QString body;
-        QString filename;
-        QString mime;
-        uint64_t media_size;
-        QString event_id;
-        TimelineItem *widget;
-        QSize dimensions;
-        bool is_encrypted = false;
-};
-
-template<class MessageT>
-MessageT
-toRoomMessage(const PendingMessage &) = delete;
-
-template<>
-mtx::events::msg::Audio
-toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::Emote
-toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::File
-toRoomMessage<mtx::events::msg::File>(const PendingMessage &);
-
-template<>
-mtx::events::msg::Image
-toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::Text
-toRoomMessage<mtx::events::msg::Text>(const PendingMessage &);
-
-template<>
-mtx::events::msg::Video
-toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m);
-
-// In which place new TimelineItems should be inserted.
-enum class TimelineDirection
-{
-        Top,
-        Bottom,
-};
-
-class TimelineView : public QWidget
-{
-        Q_OBJECT
-
-public:
-        TimelineView(const mtx::responses::Timeline &timeline,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineView(const QString &room_id, QWidget *parent = 0);
-
-        // Add new events at the end of the timeline.
-        void addEvents(const mtx::responses::Timeline &timeline);
-        void addUserMessage(mtx::events::MessageType ty,
-                            const QString &body,
-                            const RelatedInfo &related);
-        void addUserMessage(mtx::events::MessageType ty, const QString &msg);
-
-        template<class Widget, mtx::events::MessageType MsgType>
-        void addUserMessage(const QString &url,
-                            const QString &filename,
-                            const QString &mime,
-                            uint64_t size,
-                            const QSize &dimensions = QSize());
-        void updatePendingMessage(const std::string &txn_id, const QString &event_id);
-        void scrollDown();
-
-        //! Remove an item from the timeline with the given Event ID.
-        void removeEvent(const QString &event_id);
-        void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; }
-
-public slots:
-        void sliderRangeChanged(int min, int max);
-        void sliderMoved(int position);
-        void fetchHistory();
-
-        // Add old events at the top of the timeline.
-        void addBackwardsEvents(const mtx::responses::Messages &msgs);
-
-        // Whether or not the initial batch has been loaded.
-        bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; }
-
-        void handleFailedMessage(const std::string &txn_id);
-
-private slots:
-        void sendNextPendingMessage();
-
-signals:
-        void updateLastTimelineMessage(const QString &user, const DescInfo &info);
-        void messagesRetrieved(const mtx::responses::Messages &res);
-        void messageFailed(const std::string &txn_id);
-        void messageSent(const std::string &txn_id, const QString &event_id);
-        void markReadEvents(const std::vector<QString> &event_ids);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void showEvent(QShowEvent *event) override;
-        void hideEvent(QHideEvent *event) override;
-        bool event(QEvent *event) override;
-
-private:
-        using TimelineEvent = mtx::events::collections::TimelineEvents;
-
-        //! Mark our own widgets as read if they have more than one receipt.
-        void displayReadReceipts(std::vector<TimelineEvent> events);
-        //! Determine if the start of the timeline is reached from the response of /messages.
-        bool isStartOfTimeline(const mtx::responses::Messages &msgs);
-
-        QWidget *relativeWidget(QWidget *item, int dt) const;
-
-        DecryptionResult parseEncryptedEvent(
-          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
-
-        void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
-                               const std::map<std::string, std::string> &room_key,
-                               const std::map<std::string, DevicePublicKeys> &pks,
-                               const std::string &user_id,
-                               const mtx::responses::ClaimKeys &res,
-                               mtx::http::RequestErr err);
-
-        //! Callback for all message sending.
-        void sendRoomMessageHandler(const std::string &txn_id,
-                                    const mtx::responses::EventId &res,
-                                    mtx::http::RequestErr err);
-        void prepareEncryptedMessage(const PendingMessage &msg);
-
-        //! Call the /messages endpoint to fill the timeline.
-        void getMessages();
-        //! HACK: Fixing layout flickering when adding to the bottom
-        //! of the timeline.
-        void pushTimelineItem(QWidget *item, TimelineDirection dir)
-        {
-                setUpdatesEnabled(false);
-                item->hide();
-
-                if (dir == TimelineDirection::Top)
-                        scroll_layout_->insertWidget(0, item);
-                else
-                        scroll_layout_->addWidget(item);
-
-                QTimer::singleShot(0, this, [item, this]() {
-                        item->show();
-                        item->adjustSize();
-                        setUpdatesEnabled(true);
-                });
-        }
-
-        //! Decides whether or not to show or hide the scroll down button.
-        void toggleScrollDownButton();
-        void init();
-        void addTimelineItem(QWidget *item,
-                             TimelineDirection direction = TimelineDirection::Bottom);
-        void updateLastSender(const QString &user_id, TimelineDirection direction);
-        void notifyForLastEvent();
-        void notifyForLastEvent(const TimelineEvent &event);
-        //! Keep track of the sender and the timestamp of the current message.
-        void saveLastMessageInfo(const QString &sender, const QDateTime &datetime)
-        {
-                lastSender_       = sender;
-                lastMsgTimestamp_ = datetime;
-        }
-        void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime)
-        {
-                firstSender_       = sender;
-                firstMsgTimestamp_ = datetime;
-        }
-        //! Keep track of the sender and the timestamp of the current message.
-        void saveMessageInfo(const QString &sender,
-                             uint64_t origin_server_ts,
-                             TimelineDirection direction);
-
-        TimelineEvent findFirstViewableEvent(const std::vector<TimelineEvent> &events);
-        TimelineEvent findLastViewableEvent(const std::vector<TimelineEvent> &events);
-
-        //! Mark the last event as read.
-        void readLastEvent() const;
-        //! Whether or not the scrollbar is visible (non-zero height).
-        bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; }
-        //! Retrieve the event id of the last item.
-        QString getLastEventId() const;
-
-        template<class Event, class Widget>
-        TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
-
-        // TODO: Remove this eventually.
-        template<class Event>
-        TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
-
-        // For events with custom display widgets.
-        template<class Event, class Widget>
-        TimelineItem *createTimelineItem(const Event &event, bool withSender);
-
-        // For events without custom display widgets.
-        // TODO: All events should have custom widgets.
-        template<class Event>
-        TimelineItem *createTimelineItem(const Event &event, bool withSender);
-
-        // Used to determine whether or not we should prefix a message with the
-        // sender's name.
-        bool isSenderRendered(const QString &user_id,
-                              uint64_t origin_server_ts,
-                              TimelineDirection direction);
-
-        bool isPendingMessage(const std::string &txn_id,
-                              const QString &sender,
-                              const QString &userid);
-        void removePendingMessage(const std::string &txn_id);
-
-        bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); }
-
-        void handleNewUserMessage(PendingMessage msg);
-        bool isDateDifference(const QDateTime &first,
-                              const QDateTime &second = QDateTime::currentDateTime()) const;
-
-        // Return nullptr if the event couldn't be parsed.
-        QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
-                                   TimelineDirection direction);
-
-        //! Store the event id associated with the given widget.
-        void saveEventId(QWidget *widget);
-        //! Remove all widgets from the timeline layout.
-        void clearTimeline();
-
-        QVBoxLayout *top_layout_;
-        QVBoxLayout *scroll_layout_;
-
-        QScrollArea *scroll_area_;
-        QWidget *scroll_widget_;
-
-        QString firstSender_;
-        QDateTime firstMsgTimestamp_;
-        QString lastSender_;
-        QDateTime lastMsgTimestamp_;
-
-        QString room_id_;
-        QString prev_batch_token_;
-        QString local_user_;
-
-        bool isPaginationInProgress_ = false;
-
-        // Keeps track whether or not the user has visited the view.
-        bool isInitialized      = false;
-        bool isTimelineFinished = false;
-        bool isInitialSync      = true;
-
-        const int SCROLL_BAR_GAP = 200;
-
-        QTimer *paginationTimer_;
-
-        int scroll_height_       = 0;
-        int previous_max_height_ = 0;
-
-        int oldPosition_;
-        int oldHeight_;
-
-        FloatingButton *scrollDownBtn_;
-
-        TimelineDirection lastMessageDirection_;
-
-        //! Messages received by sync not added to the timeline.
-        std::vector<TimelineEvent> bottomMessages_;
-        //! Messages received by /messages not added to the timeline.
-        std::vector<TimelineEvent> topMessages_;
-
-        //! Render the given timeline events to the bottom of the timeline.
-        void renderBottomEvents(const std::vector<TimelineEvent> &events);
-        //! Render the given timeline events to the top of the timeline.
-        void renderTopEvents(const std::vector<TimelineEvent> &events);
-
-        // The events currently rendered. Used for duplicate detection.
-        QMap<QString, QWidget *> eventIds_;
-        QQueue<PendingMessage> pending_msgs_;
-        QList<PendingMessage> pending_sent_msgs_;
-};
-
-template<class Widget, mtx::events::MessageType MsgType>
-void
-TimelineView::addUserMessage(const QString &url,
-                             const QString &filename,
-                             const QString &mime,
-                             uint64_t size,
-                             const QSize &dimensions)
-{
-        auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_);
-        auto trimmed     = QFileInfo{filename}.fileName(); // Trim file path.
-
-        auto widget = new Widget(url, trimmed, size, this);
-
-        TimelineItem *view_item =
-          new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_);
-
-        addTimelineItem(view_item);
-
-        lastMessageDirection_ = TimelineDirection::Bottom;
-
-        // Keep track of the sender and the timestamp of the current message.
-        saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
-
-        PendingMessage message;
-        message.ty         = MsgType;
-        message.txn_id     = http::client()->generate_txn_id();
-        message.body       = url;
-        message.filename   = trimmed;
-        message.mime       = mime;
-        message.media_size = size;
-        message.widget     = view_item;
-        message.dimensions = dimensions;
-
-        handleNewUserMessage(message);
-}
-
-template<class Event>
-TimelineItem *
-TimelineView::createTimelineItem(const Event &event, bool withSender)
-{
-        TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_);
-        return item;
-}
-
-template<class Event, class Widget>
-TimelineItem *
-TimelineView::createTimelineItem(const Event &event, bool withSender)
-{
-        auto eventWidget = new Widget(event);
-        auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_);
-
-        return item;
-}
-
-template<class Event>
-TimelineItem *
-TimelineView::processMessageEvent(const Event &event, TimelineDirection direction)
-{
-        const auto event_id = QString::fromStdString(event.event_id);
-        const auto sender   = QString::fromStdString(event.sender);
-
-        const auto txn_id = event.unsigned_data.transaction_id;
-        if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
-            isDuplicate(event_id)) {
-                removePendingMessage(txn_id);
-                return nullptr;
-        }
-
-        auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction);
-
-        saveMessageInfo(sender, event.origin_server_ts, direction);
-
-        auto item = createTimelineItem<Event>(event, with_sender);
-
-        eventIds_[event_id] = item;
-
-        return item;
-}
-
-template<class Event, class Widget>
-TimelineItem *
-TimelineView::processMessageEvent(const Event &event, TimelineDirection direction)
-{
-        const auto event_id = QString::fromStdString(event.event_id);
-        const auto sender   = QString::fromStdString(event.sender);
-
-        const auto txn_id = event.unsigned_data.transaction_id;
-        if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
-            isDuplicate(event_id)) {
-                removePendingMessage(txn_id);
-                return nullptr;
-        }
-
-        auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction);
-
-        saveMessageInfo(sender, event.origin_server_ts, direction);
-
-        auto item = createTimelineItem<Event, Widget>(event, with_sender);
-
-        eventIds_[event_id] = item;
-
-        return item;
-}
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 8650548175c814165364689eec8f8ea6b1956c23..6e18d111f1692760ff630ccfbc51eecd57a05521 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -1,340 +1,292 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
+#include "TimelineViewManager.h"
 
-#include <random>
+#include <QMetaType>
+#include <QPalette>
+#include <QQmlContext>
 
-#include <QApplication>
-#include <QFileInfo>
-#include <QSettings>
-
-#include "Cache.h"
+#include "ChatPage.h"
+#include "ColorImageProvider.h"
+#include "DelegateChooser.h"
 #include "Logging.h"
-#include "Utils.h"
-#include "timeline/TimelineView.h"
-#include "timeline/TimelineViewManager.h"
-#include "timeline/widgets/AudioItem.h"
-#include "timeline/widgets/FileItem.h"
-#include "timeline/widgets/ImageItem.h"
-#include "timeline/widgets/VideoItem.h"
-
-TimelineViewManager::TimelineViewManager(QWidget *parent)
-  : QStackedWidget(parent)
-{}
+#include "MxcImageProvider.h"
+#include "UserSettingsPage.h"
+#include "dialogs/ImageOverlay.h"
 
 void
-TimelineViewManager::updateReadReceipts(const QString &room_id,
-                                        const std::vector<QString> &event_ids)
+TimelineViewManager::updateColorPalette()
 {
-        if (timelineViewExists(room_id)) {
-                auto view = views_[room_id];
-                if (view)
-                        emit view->markReadEvents(event_ids);
+        UserSettings settings;
+        if (settings.theme() == "light") {
+                QPalette lightActive(/*windowText*/ QColor("#333"),
+                                     /*button*/ QColor("#333"),
+                                     /*light*/ QColor(),
+                                     /*dark*/ QColor(220, 220, 220, 120),
+                                     /*mid*/ QColor(),
+                                     /*text*/ QColor("#333"),
+                                     /*bright_text*/ QColor(),
+                                     /*base*/ QColor("white"),
+                                     /*window*/ QColor("white"));
+                view->rootContext()->setContextProperty("currentActivePalette", lightActive);
+                view->rootContext()->setContextProperty("currentInactivePalette", lightActive);
+        } else if (settings.theme() == "dark") {
+                QPalette darkActive(/*windowText*/ QColor("#caccd1"),
+                                    /*button*/ QColor("#caccd1"),
+                                    /*light*/ QColor(),
+                                    /*dark*/ QColor(45, 49, 57, 120),
+                                    /*mid*/ QColor(),
+                                    /*text*/ QColor("#caccd1"),
+                                    /*bright_text*/ QColor(),
+                                    /*base*/ QColor("#202228"),
+                                    /*window*/ QColor("#202228"));
+                darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9"));
+                view->rootContext()->setContextProperty("currentActivePalette", darkActive);
+                view->rootContext()->setContextProperty("currentInactivePalette", darkActive);
+        } else {
+                view->rootContext()->setContextProperty("currentActivePalette", QPalette());
+                view->rootContext()->setContextProperty("currentInactivePalette", nullptr);
         }
 }
 
-void
-TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
-{
-        auto view = views_[room_id];
-
-        if (view)
-                view->removeEvent(event_id);
-}
-
-void
-TimelineViewManager::queueTextMessage(const QString &msg)
-{
-        if (active_room_.isEmpty())
-                return;
-
-        auto room_id = active_room_;
-        auto view    = views_[room_id];
-
-        view->addUserMessage(mtx::events::MessageType::Text, msg);
-}
-
-void
-TimelineViewManager::queueEmoteMessage(const QString &msg)
-{
-        if (active_room_.isEmpty())
-                return;
-
-        auto room_id = active_room_;
-        auto view    = views_[room_id];
-
-        view->addUserMessage(mtx::events::MessageType::Emote, msg);
-}
-
-void
-TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
+TimelineViewManager::TimelineViewManager(QWidget *parent)
+  : imgProvider(new MxcImageProvider())
+  , colorImgProvider(new ColorImageProvider())
 {
-        if (active_room_.isEmpty())
-                return;
-
-        auto room_id = active_room_;
-        auto view    = views_[room_id];
-
-        view->addUserMessage(mtx::events::MessageType::Text, reply, related);
+        qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
+                                         "im.nheko",
+                                         1,
+                                         0,
+                                         "MtxEvent",
+                                         "Can't instantiate enum!");
+        qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
+        qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
+
+#ifdef USE_QUICK_VIEW
+        view      = new QQuickView();
+        container = QWidget::createWindowContainer(view, parent);
+#else
+        view      = new QQuickWidget(parent);
+        container = view;
+        view->setResizeMode(QQuickWidget::SizeRootObjectToView);
+        container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+        connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
+                nhlog::ui()->debug("Status changed to {}", status);
+        });
+#endif
+        container->setMinimumSize(200, 200);
+        view->rootContext()->setContextProperty("timelineManager", this);
+        updateColorPalette();
+        view->engine()->addImageProvider("MxcImage", imgProvider);
+        view->engine()->addImageProvider("colorimage", colorImgProvider);
+        view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
+
+        connect(dynamic_cast<ChatPage *>(parent),
+                &ChatPage::themeChanged,
+                this,
+                &TimelineViewManager::updateColorPalette);
 }
 
 void
-TimelineViewManager::queueImageMessage(const QString &roomid,
-                                       const QString &filename,
-                                       const QString &url,
-                                       const QString &mime,
-                                       uint64_t size,
-                                       const QSize &dimensions)
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
 {
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("Cannot send m.image message to a non-managed view");
-                return;
+        for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
+                // addRoom will only add the room, if it doesn't exist
+                addRoom(QString::fromStdString(it->first));
+                models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline);
         }
 
-        auto view = views_[roomid];
-
-        view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(
-          url, filename, mime, size, dimensions);
+        this->isInitialSync_ = false;
+        emit initialSyncChanged(false);
 }
 
 void
-TimelineViewManager::queueFileMessage(const QString &roomid,
-                                      const QString &filename,
-                                      const QString &url,
-                                      const QString &mime,
-                                      uint64_t size)
+TimelineViewManager::addRoom(const QString &room_id)
 {
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("cannot send m.file message to a non-managed view");
-                return;
+        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));
         }
-
-        auto view = views_[roomid];
-
-        view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size);
 }
 
 void
-TimelineViewManager::queueAudioMessage(const QString &roomid,
-                                       const QString &filename,
-                                       const QString &url,
-                                       const QString &mime,
-                                       uint64_t size)
+TimelineViewManager::setHistoryView(const QString &room_id)
 {
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("cannot send m.audio message to a non-managed view");
-                return;
-        }
-
-        auto view = views_[roomid];
+        nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
 
-        view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size);
+        auto room = models.find(room_id);
+        if (room != models.end()) {
+                timeline_ = room.value().data();
+                emit activeTimelineChanged(timeline_);
+                nhlog::ui()->info("Activated room {}", room_id.toStdString());
+        }
 }
 
 void
-TimelineViewManager::queueVideoMessage(const QString &roomid,
-                                       const QString &filename,
-                                       const QString &url,
-                                       const QString &mime,
-                                       uint64_t size)
+TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
 {
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("cannot send m.video message to a non-managed view");
-                return;
-        }
-
-        auto view = views_[roomid];
+        QQuickImageResponse *imgResponse =
+          imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
+        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());
 
-        view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size);
+                auto imgDialog = new dialogs::ImageOverlay(pixmap);
+                imgDialog->show();
+                connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() {
+                        timeline_->saveMedia(eventId);
+                });
+        });
 }
 
 void
-TimelineViewManager::initialize(const mtx::responses::Rooms &rooms)
+TimelineViewManager::updateReadReceipts(const QString &room_id,
+                                        const std::vector<QString> &event_ids)
 {
-        for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
-                addRoom(it->second, QString::fromStdString(it->first));
+        auto room = models.find(room_id);
+        if (room != models.end()) {
+                room.value()->markEventsAsRead(event_ids);
         }
-
-        sync(rooms);
 }
 
 void
 TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
 {
-        for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) {
-                if (timelineViewExists(it->first))
-                        return;
+        for (const auto &e : msgs) {
+                addRoom(e.first);
 
-                // Create a history view with the room events.
-                TimelineView *view = new TimelineView(it->second, it->first);
-                views_.emplace(it->first, QSharedPointer<TimelineView>(view));
-
-                connect(view,
-                        &TimelineView::updateLastTimelineMessage,
-                        this,
-                        &TimelineViewManager::updateRoomsLastMessage);
-
-                // Add the view in the widget stack.
-                addWidget(view);
+                models.value(e.first)->addEvents(e.second);
         }
 }
 
 void
-TimelineViewManager::initialize(const std::vector<std::string> &rooms)
+TimelineViewManager::queueTextMessage(const QString &msg)
 {
-        for (const auto &roomid : rooms)
-                addRoom(QString::fromStdString(roomid));
+        mtx::events::msg::Text text = {};
+        text.body                   = msg.trimmed().toStdString();
+        text.format                 = "org.matrix.custom.html";
+        text.formatted_body         = utils::markdownToHtml(msg).toStdString();
+
+        if (timeline_)
+                timeline_->sendMessage(text);
 }
 
 void
-TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id)
+TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
 {
-        if (timelineViewExists(room_id))
-                return;
-
-        // Create a history view with the room events.
-        TimelineView *view = new TimelineView(room.timeline, room_id);
-        views_.emplace(room_id, QSharedPointer<TimelineView>(view));
+        mtx::events::msg::Text text = {};
+
+        QString body;
+        bool firstLine = true;
+        for (const auto &line : related.quoted_body.split("\n")) {
+                if (firstLine) {
+                        firstLine = false;
+                        body      = QString("> <%1> %2\n").arg(related.quoted_user).arg(line);
+                } else {
+                        body = QString("%1\n> %2\n").arg(body).arg(line);
+                }
+        }
 
-        connect(view,
-                &TimelineView::updateLastTimelineMessage,
-                this,
-                &TimelineViewManager::updateRoomsLastMessage);
+        text.body   = QString("%1\n%2").arg(body).arg(reply).toStdString();
+        text.format = "org.matrix.custom.html";
+        text.formatted_body =
+          utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString();
+        text.relates_to.in_reply_to.event_id = related.related_event;
 
-        // Add the view in the widget stack.
-        addWidget(view);
+        if (timeline_)
+                timeline_->sendMessage(text);
 }
 
 void
-TimelineViewManager::addRoom(const QString &room_id)
+TimelineViewManager::queueEmoteMessage(const QString &msg)
 {
-        if (timelineViewExists(room_id))
-                return;
+        auto html = utils::markdownToHtml(msg);
 
-        // Create a history view without any events.
-        TimelineView *view = new TimelineView(room_id);
-        views_.emplace(room_id, QSharedPointer<TimelineView>(view));
+        mtx::events::msg::Emote emote;
+        emote.body = msg.trimmed().toStdString();
 
-        connect(view,
-                &TimelineView::updateLastTimelineMessage,
-                this,
-                &TimelineViewManager::updateRoomsLastMessage);
+        if (html != msg.trimmed().toHtmlEscaped())
+                emote.formatted_body = html.toStdString();
 
-        // Add the view in the widget stack.
-        addWidget(view);
+        if (timeline_)
+                timeline_->sendMessage(emote);
 }
 
 void
-TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
+TimelineViewManager::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)
 {
-        for (const auto &room : rooms.join) {
-                auto roomid = QString::fromStdString(room.first);
-
-                if (!timelineViewExists(roomid)) {
-                        nhlog::ui()->warn("ignoring event from unknown room: {}",
-                                          roomid.toStdString());
-                        continue;
-                }
-
-                auto view = views_.at(roomid);
-
-                view->addEvents(room.second.timeline);
-        }
+        mtx::events::msg::Image image;
+        image.info.mimetype = mime.toStdString();
+        image.info.size     = dsize;
+        image.body          = filename.toStdString();
+        image.url           = url.toStdString();
+        image.info.h        = dimensions.height();
+        image.info.w        = dimensions.width();
+        image.file          = file;
+        models.value(roomid)->sendMessage(image);
 }
 
 void
-TimelineViewManager::setHistoryView(const QString &room_id)
+TimelineViewManager::queueFileMessage(
+  const QString &roomid,
+  const QString &filename,
+  const boost::optional<mtx::crypto::EncryptedFile> &encryptedFile,
+  const QString &url,
+  const QString &mime,
+  uint64_t dsize)
 {
-        if (!timelineViewExists(room_id)) {
-                nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}",
-                                  room_id.toStdString());
-                return;
-        }
-
-        active_room_ = room_id;
-        auto view    = views_.at(room_id);
-
-        setCurrentWidget(view.data());
-
-        view->fetchHistory();
-        view->scrollDown();
+        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);
 }
 
-QString
-TimelineViewManager::chooseRandomColor()
+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)
 {
-        std::random_device random_device;
-        std::mt19937 engine{random_device()};
-        std::uniform_real_distribution<float> dist(0, 1);
-
-        float hue        = dist(engine);
-        float saturation = 0.9;
-        float value      = 0.7;
-
-        int hue_i = hue * 6;
-
-        float f = hue * 6 - hue_i;
-
-        float p = value * (1 - saturation);
-        float q = value * (1 - f * saturation);
-        float t = value * (1 - (1 - f) * saturation);
-
-        float r = 0;
-        float g = 0;
-        float b = 0;
-
-        if (hue_i == 0) {
-                r = value;
-                g = t;
-                b = p;
-        } else if (hue_i == 1) {
-                r = q;
-                g = value;
-                b = p;
-        } else if (hue_i == 2) {
-                r = p;
-                g = value;
-                b = t;
-        } else if (hue_i == 3) {
-                r = p;
-                g = q;
-                b = value;
-        } else if (hue_i == 4) {
-                r = t;
-                g = p;
-                b = value;
-        } else if (hue_i == 5) {
-                r = value;
-                g = p;
-                b = q;
-        }
-
-        int ri = r * 256;
-        int gi = g * 256;
-        int bi = b * 256;
-
-        QColor color(ri, gi, bi);
-
-        return color.name();
+        mtx::events::msg::Audio audio;
+        audio.info.mimetype = mime.toStdString();
+        audio.info.size     = dsize;
+        audio.body          = filename.toStdString();
+        audio.url           = url.toStdString();
+        audio.file          = file;
+        models.value(roomid)->sendMessage(audio);
 }
 
-bool
-TimelineViewManager::hasLoaded() const
+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)
 {
-        return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) {
-                return view.second->hasLoaded();
-        });
+        mtx::events::msg::Video video;
+        video.info.mimetype = mime.toStdString();
+        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 b52136d97a3112fe2778b2b10b602ecedf247424..9e8de616c08974abaf51a320136047ac4e12dd2d 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -1,98 +1,97 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 #pragma once
 
+#include <QQuickView>
+#include <QQuickWidget>
 #include <QSharedPointer>
-#include <QStackedWidget>
+#include <QWidget>
 
-#include <mtx.hpp>
+#include <mtx/common.hpp>
+#include <mtx/responses.hpp>
 
+#include "Cache.h"
+#include "Logging.h"
+#include "TimelineModel.h"
 #include "Utils.h"
 
-class QFile;
-
-class RoomInfoListItem;
-class TimelineView;
-struct DescInfo;
-struct SavedMessages;
+class MxcImageProvider;
+class ColorImageProvider;
 
-class TimelineViewManager : public QStackedWidget
+class TimelineViewManager : public QObject
 {
         Q_OBJECT
 
-public:
-        TimelineViewManager(QWidget *parent);
+        Q_PROPERTY(
+          TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
+        Q_PROPERTY(
+          bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
 
-        // Initialize with timeline events.
-        void initialize(const mtx::responses::Rooms &rooms);
-        // Empty initialization.
-        void initialize(const std::vector<std::string> &rooms);
-
-        void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id);
-        void addRoom(const QString &room_id);
+public:
+        TimelineViewManager(QWidget *parent = 0);
+        QWidget *getWidget() const { return container; }
 
         void sync(const mtx::responses::Rooms &rooms);
-        void clearAll() { views_.clear(); }
+        void addRoom(const QString &room_id);
 
-        // Check if all the timelines have been loaded.
-        bool hasLoaded() const;
+        void clearAll() { models.clear(); }
 
-        static QString chooseRandomColor();
+        Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
+        Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
+        Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
 
 signals:
         void clearRoomMessageCount(QString roomid);
-        void updateRoomsLastMessage(const QString &user, const DescInfo &info);
+        void updateRoomsLastMessage(QString roomid, const DescInfo &info);
+        void activeTimelineChanged(TimelineModel *timeline);
+        void initialSyncChanged(bool isInitialSync);
 
 public slots:
         void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
-        void removeTimelineEvent(const QString &room_id, const QString &event_id);
         void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);
 
         void setHistoryView(const QString &room_id);
+        void updateColorPalette();
+
         void queueTextMessage(const QString &msg);
         void queueReplyMessage(const QString &reply, const RelatedInfo &related);
         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);
 
 private:
-        //! Check if the given room id is managed by a TimelineView.
-        bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }
-
-        QString active_room_;
-        std::map<QString, QSharedPointer<TimelineView>> views_;
+#ifdef USE_QUICK_VIEW
+        QQuickView *view;
+#else
+        QQuickWidget *view;
+#endif
+        QWidget *container;
+
+        MxcImageProvider *imgProvider;
+        ColorImageProvider *colorImgProvider;
+
+        QHash<QString, QSharedPointer<TimelineModel>> models;
+        TimelineModel *timeline_ = nullptr;
+        bool isInitialSync_      = true;
 };
diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp
deleted file mode 100644
index 5d6431ee4c881b4fffe398b908747459d9dc4dda..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/AudioItem.cpp
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFile>
-#include <QFileDialog>
-#include <QPainter>
-#include <QPixmap>
-#include <QtGlobal>
-
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-
-#include "timeline/widgets/AudioItem.h"
-
-constexpr int MaxWidth          = 400;
-constexpr int Height            = 70;
-constexpr int IconRadius        = 22;
-constexpr int IconDiameter      = IconRadius * 2;
-constexpr int HorizontalPadding = 12;
-constexpr int TextPadding       = 15;
-constexpr int ActionIconRadius  = IconRadius - 4;
-
-constexpr double VerticalPadding = Height - 2 * IconRadius;
-constexpr double IconYCenter     = Height / 2;
-constexpr double IconXCenter     = HorizontalPadding + IconRadius;
-
-void
-AudioItem::init()
-{
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
-
-        playIcon_.addFile(":/icons/icons/ui/play-sign.png");
-        pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
-
-        player_ = new QMediaPlayer;
-        player_->setMedia(QUrl(url_));
-        player_->setVolume(100);
-        player_->setNotifyInterval(1000);
-
-        connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
-                if (state == QMediaPlayer::StoppedState) {
-                        state_ = AudioState::Play;
-                        player_->setMedia(QUrl(url_));
-                        update();
-                }
-        });
-
-        setFixedHeight(Height);
-}
-
-AudioItem::AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, QWidget *parent)
-  : QWidget(parent)
-  , url_{QUrl(QString::fromStdString(event.content.url))}
-  , text_{QString::fromStdString(event.content.body)}
-  , event_{event}
-{
-        readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
-
-        init();
-}
-
-AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        readableFileSize_ = utils::humanReadableFileSize(size);
-
-        init();
-}
-
-QSize
-AudioItem::sizeHint() const
-{
-        return QSize(MaxWidth, Height);
-}
-
-void
-AudioItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->button() != Qt::LeftButton)
-                return;
-
-        auto point = event->pos();
-
-        // Click on the download icon.
-        if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter)
-              .contains(point)) {
-                if (state_ == AudioState::Play) {
-                        state_ = AudioState::Pause;
-                        player_->play();
-                } else {
-                        state_ = AudioState::Play;
-                        player_->pause();
-                }
-
-                update();
-        } else {
-                filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_);
-
-                if (filenameToSave_.isEmpty())
-                        return;
-
-                auto proxy = std::make_shared<MediaProxy>();
-                connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded);
-
-                http::client()->download(
-                  url_.toString().toStdString(),
-                  [proxy = std::move(proxy), url = url_](const std::string &data,
-                                                         const std::string &,
-                                                         const std::string &,
-                                                         mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::net()->info("failed to retrieve m.audio content: {}",
-                                                     url.toString().toStdString());
-                                  return;
-                          }
-
-                          emit proxy->fileDownloaded(QByteArray(data.data(), data.size()));
-                  });
-        }
-}
-
-void
-AudioItem::fileDownloaded(const QByteArray &data)
-{
-        try {
-                QFile file(filenameToSave_);
-
-                if (!file.open(QIODevice::WriteOnly))
-                        return;
-
-                file.write(data);
-                file.close();
-        } catch (const std::exception &e) {
-                nhlog::ui()->warn("error while saving file: {}", e.what());
-        }
-}
-
-void
-AudioItem::resizeEvent(QResizeEvent *event)
-{
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-        const int computedWidth = std::min(
-          fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth);
-#else
-        const int computedWidth =
-          std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding,
-                   (double)MaxWidth);
-#endif
-        resize(computedWidth, Height);
-
-        event->accept();
-}
-
-void
-AudioItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-
-        QPainterPath path;
-        path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(path, backgroundColor_);
-        painter.drawPath(path);
-
-        QPainterPath circle;
-        circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(circle, iconColor_);
-        painter.drawPath(circle);
-
-        QIcon icon_;
-        if (state_ == AudioState::Play)
-                icon_ = playIcon_;
-        else
-                icon_ = pauseIcon_;
-
-        icon_.paint(&painter,
-                    QRect(IconXCenter - ActionIconRadius / 2,
-                          IconYCenter - ActionIconRadius / 2,
-                          ActionIconRadius,
-                          ActionIconRadius),
-                    Qt::AlignCenter,
-                    QIcon::Normal);
-
-        const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding;
-        const int textStartY = VerticalPadding + fm.ascent() / 2;
-
-        // Draw the filename.
-        QString elidedText = fm.elidedText(
-          text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius);
-
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY), elidedText);
-
-        // Draw the filesize.
-        font.setWeight(QFont::Normal);
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_);
-}
diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h
deleted file mode 100644
index c32b773137a8310037045847ced825b4d9f7ebaa..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/AudioItem.h
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QIcon>
-#include <QMediaPlayer>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-class AudioItem : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-
-        Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ
-                     durationBackgroundColor)
-        Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ
-                     durationForegroundColor)
-
-public:
-        AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
-                  QWidget *parent = nullptr);
-
-        AudioItem(const QString &url,
-                  const QString &filename,
-                  uint64_t size,
-                  QWidget *parent = nullptr);
-
-        QSize sizeHint() const override;
-
-        void setTextColor(const QColor &color) { textColor_ = color; }
-        void setIconColor(const QColor &color) { iconColor_ = color; }
-        void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
-
-        void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; }
-        void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; }
-
-        QColor textColor() const { return textColor_; }
-        QColor iconColor() const { return iconColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        QColor durationBackgroundColor() const { return durationBgColor_; }
-        QColor durationForegroundColor() const { return durationFgColor_; }
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
-
-private slots:
-        void fileDownloaded(const QByteArray &data);
-
-private:
-        void init();
-
-        enum class AudioState
-        {
-                Play,
-                Pause,
-        };
-
-        AudioState state_ = AudioState::Play;
-
-        QUrl url_;
-        QString text_;
-        QString readableFileSize_;
-        QString filenameToSave_;
-
-        mtx::events::RoomEvent<mtx::events::msg::Audio> event_;
-
-        QMediaPlayer *player_;
-
-        QIcon playIcon_;
-        QIcon pauseIcon_;
-
-        QColor textColor_       = QColor("white");
-        QColor iconColor_       = QColor("#38A3D8");
-        QColor backgroundColor_ = QColor("#333");
-
-        QColor durationBgColor_ = QColor("black");
-        QColor durationFgColor_ = QColor("blue");
-};
diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp
deleted file mode 100644
index 1a555d1cf099f0f9a7f260b8182fe3eeeb953855..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/FileItem.cpp
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFile>
-#include <QFileDialog>
-#include <QPainter>
-#include <QPixmap>
-#include <QtGlobal>
-
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-
-#include "timeline/widgets/FileItem.h"
-
-constexpr int MaxWidth           = 400;
-constexpr int Height             = 70;
-constexpr int IconRadius         = 22;
-constexpr int IconDiameter       = IconRadius * 2;
-constexpr int HorizontalPadding  = 12;
-constexpr int TextPadding        = 15;
-constexpr int DownloadIconRadius = IconRadius - 4;
-
-constexpr double VerticalPadding = Height - 2 * IconRadius;
-constexpr double IconYCenter     = Height / 2;
-constexpr double IconXCenter     = HorizontalPadding + IconRadius;
-
-void
-FileItem::init()
-{
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
-
-        icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
-
-        setFixedHeight(Height);
-}
-
-FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
-  : QWidget(parent)
-  , url_{QString::fromStdString(event.content.url)}
-  , text_{QString::fromStdString(event.content.body)}
-  , event_{event}
-{
-        readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
-
-        init();
-}
-
-FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        readableFileSize_ = utils::humanReadableFileSize(size);
-
-        init();
-}
-
-void
-FileItem::openUrl()
-{
-        if (url_.toString().isEmpty())
-                return;
-
-        auto urlToOpen = utils::mxcToHttp(
-          url_, QString::fromStdString(http::client()->server()), http::client()->port());
-
-        if (!QDesktopServices::openUrl(urlToOpen))
-                nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString());
-}
-
-QSize
-FileItem::sizeHint() const
-{
-        return QSize(MaxWidth, Height);
-}
-
-void
-FileItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->button() != Qt::LeftButton)
-                return;
-
-        auto point = event->pos();
-
-        // Click on the download icon.
-        if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter)
-              .contains(point)) {
-                filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_);
-
-                if (filenameToSave_.isEmpty())
-                        return;
-
-                auto proxy = std::make_shared<MediaProxy>();
-                connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded);
-
-                http::client()->download(
-                  url_.toString().toStdString(),
-                  [proxy = std::move(proxy), url = url_](const std::string &data,
-                                                         const std::string &,
-                                                         const std::string &,
-                                                         mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::ui()->warn("failed to retrieve m.file content: {}",
-                                                    url.toString().toStdString());
-                                  return;
-                          }
-
-                          emit proxy->fileDownloaded(QByteArray(data.data(), data.size()));
-                  });
-        } else {
-                openUrl();
-        }
-}
-
-void
-FileItem::fileDownloaded(const QByteArray &data)
-{
-        try {
-                QFile file(filenameToSave_);
-
-                if (!file.open(QIODevice::WriteOnly))
-                        return;
-
-                file.write(data);
-                file.close();
-        } catch (const std::exception &e) {
-                nhlog::ui()->warn("Error while saving file to: {}", e.what());
-        }
-}
-
-void
-FileItem::resizeEvent(QResizeEvent *event)
-{
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-        const int computedWidth = std::min(
-          fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth);
-#else
-        const int computedWidth =
-          std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding,
-                   (double)MaxWidth);
-#endif
-        resize(computedWidth, Height);
-
-        event->accept();
-}
-
-void
-FileItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-
-        QPainterPath path;
-        path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(path, backgroundColor_);
-        painter.drawPath(path);
-
-        QPainterPath circle;
-        circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(circle, iconColor_);
-        painter.drawPath(circle);
-
-        icon_.paint(&painter,
-                    QRect(IconXCenter - DownloadIconRadius / 2,
-                          IconYCenter - DownloadIconRadius / 2,
-                          DownloadIconRadius,
-                          DownloadIconRadius),
-                    Qt::AlignCenter,
-                    QIcon::Normal);
-
-        const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding;
-        const int textStartY = VerticalPadding + fm.ascent() / 2;
-
-        // Draw the filename.
-        QString elidedText = fm.elidedText(
-          text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius);
-
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY), elidedText);
-
-        // Draw the filesize.
-        font.setWeight(QFont::Normal);
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_);
-}
diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h
deleted file mode 100644
index d63cce8895c5c7f0ca32c299f1d07947a01dbad3..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/FileItem.h
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QIcon>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-class FileItem : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-
-public:
-        FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event,
-                 QWidget *parent = nullptr);
-
-        FileItem(const QString &url,
-                 const QString &filename,
-                 uint64_t size,
-                 QWidget *parent = nullptr);
-
-        QSize sizeHint() const override;
-
-        void setTextColor(const QColor &color) { textColor_ = color; }
-        void setIconColor(const QColor &color) { iconColor_ = color; }
-        void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
-
-        QColor textColor() const { return textColor_; }
-        QColor iconColor() const { return iconColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-
-private slots:
-        void fileDownloaded(const QByteArray &data);
-
-private:
-        void openUrl();
-        void init();
-
-        QUrl url_;
-        QString text_;
-        QString readableFileSize_;
-        QString filenameToSave_;
-
-        mtx::events::RoomEvent<mtx::events::msg::File> event_;
-
-        QIcon icon_;
-
-        QColor textColor_       = QColor("white");
-        QColor iconColor_       = QColor("#38A3D8");
-        QColor backgroundColor_ = QColor("#333");
-};
diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp
deleted file mode 100644
index 26c569d7e8dce0ec87eb39951392dfe2e824ec35..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/ImageItem.cpp
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFileDialog>
-#include <QFileInfo>
-#include <QPainter>
-#include <QPixmap>
-#include <QUuid>
-#include <QtGlobal>
-
-#include "Config.h"
-#include "ImageItem.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-#include "dialogs/ImageOverlay.h"
-
-void
-ImageItem::downloadMedia(const QUrl &url)
-{
-        auto proxy = std::make_shared<MediaProxy>();
-        connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage);
-
-        http::client()->download(url.toString().toStdString(),
-                                 [proxy = std::move(proxy), url](const std::string &data,
-                                                                 const std::string &,
-                                                                 const std::string &,
-                                                                 mtx::http::RequestErr err) {
-                                         if (err) {
-                                                 nhlog::net()->warn(
-                                                   "failed to retrieve image {}: {} {}",
-                                                   url.toString().toStdString(),
-                                                   err->matrix_error.error,
-                                                   static_cast<int>(err->status_code));
-                                                 return;
-                                         }
-
-                                         QPixmap img;
-                                         img.loadFromData(QByteArray(data.data(), data.size()));
-
-                                         emit proxy->imageDownloaded(img);
-                                 });
-}
-
-void
-ImageItem::saveImage(const QString &filename, const QByteArray &data)
-{
-        try {
-                QFile file(filename);
-
-                if (!file.open(QIODevice::WriteOnly))
-                        return;
-
-                file.write(data);
-                file.close();
-        } catch (const std::exception &e) {
-                nhlog::ui()->warn("Error while saving file to: {}", e.what());
-        }
-}
-
-void
-ImageItem::init()
-{
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
-
-        downloadMedia(url_);
-}
-
-ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
-  : QWidget(parent)
-  , event_{event}
-{
-        url_  = QString::fromStdString(event.content.url);
-        text_ = QString::fromStdString(event.content.body);
-
-        init();
-}
-
-ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        Q_UNUSED(size);
-        init();
-}
-
-void
-ImageItem::openUrl()
-{
-        if (url_.toString().isEmpty())
-                return;
-
-        auto urlToOpen = utils::mxcToHttp(
-          url_, QString::fromStdString(http::client()->server()), http::client()->port());
-
-        if (!QDesktopServices::openUrl(urlToOpen))
-                nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString());
-}
-
-QSize
-ImageItem::sizeHint() const
-{
-        if (image_.isNull())
-                return QSize(max_width_, bottom_height_);
-
-        return QSize(width_, height_);
-}
-
-void
-ImageItem::setImage(const QPixmap &image)
-{
-        image_        = image;
-        scaled_image_ = utils::scaleDown(max_width_, max_height_, image_);
-
-        width_  = scaled_image_.width();
-        height_ = scaled_image_.height();
-
-        setFixedSize(width_, height_);
-        update();
-}
-
-void
-ImageItem::mousePressEvent(QMouseEvent *event)
-{
-        if (!isInteractive_) {
-                event->accept();
-                return;
-        }
-
-        if (event->button() != Qt::LeftButton)
-                return;
-
-        if (image_.isNull()) {
-                openUrl();
-                return;
-        }
-
-        if (textRegion_.contains(event->pos())) {
-                openUrl();
-        } else {
-                auto imgDialog = new dialogs::ImageOverlay(image_);
-                imgDialog->show();
-                connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs);
-        }
-}
-
-void
-ImageItem::resizeEvent(QResizeEvent *event)
-{
-        if (!image_)
-                return QWidget::resizeEvent(event);
-
-        scaled_image_ = utils::scaleDown(max_width_, max_height_, image_);
-
-        width_  = scaled_image_.width();
-        height_ = scaled_image_.height();
-
-        setFixedSize(width_, height_);
-}
-
-void
-ImageItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-
-        QFont font;
-
-        QFontMetrics metrics(font);
-        const int fontHeight = metrics.height() + metrics.ascent();
-
-        if (image_.isNull()) {
-                QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-                setFixedSize(metrics.width(elidedText), fontHeight);
-#else
-                setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight);
-#endif
-                painter.setFont(font);
-                painter.setPen(QPen(QColor(66, 133, 244)));
-                painter.drawText(QPoint(0, fontHeight / 2), elidedText);
-
-                return;
-        }
-
-        imageRegion_ = QRectF(0, 0, width_, height_);
-
-        QPainterPath path;
-        path.addRoundedRect(imageRegion_, 5, 5);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(path, scaled_image_);
-        painter.drawPath(path);
-
-        // Bottom text section
-        if (isInteractive_ && underMouse()) {
-                const int textBoxHeight = fontHeight / 2 + 6;
-
-                textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight);
-
-                QPainterPath textPath;
-                textPath.addRoundedRect(textRegion_, 0, 0);
-
-                painter.fillPath(textPath, QColor(40, 40, 40, 140));
-
-                QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10);
-
-                font.setWeight(QFont::Medium);
-                painter.setFont(font);
-                painter.setPen(QPen(QColor(Qt::white)));
-
-                textRegion_.adjust(5, 0, 5, 0);
-                painter.drawText(textRegion_, Qt::AlignVCenter, elidedText);
-        }
-}
-
-void
-ImageItem::saveAs()
-{
-        auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_);
-
-        if (filename.isEmpty())
-                return;
-
-        const auto url = url_.toString().toStdString();
-
-        auto proxy = std::make_shared<MediaProxy>();
-        connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage);
-
-        http::client()->download(
-          url,
-          [proxy = std::move(proxy), filename, url](const std::string &data,
-                                                    const std::string &,
-                                                    const std::string &,
-                                                    mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
-                                             url,
-                                             err->matrix_error.error,
-                                             static_cast<int>(err->status_code));
-                          return;
-                  }
-
-                  emit proxy->imageSaved(filename, QByteArray(data.data(), data.size()));
-          });
-}
diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h
deleted file mode 100644
index 65bd962de528bbb4b9f2a4302eb01a9542687c15..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/ImageItem.h
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-namespace dialogs {
-class ImageOverlay;
-}
-
-class ImageItem : public QWidget
-{
-        Q_OBJECT
-public:
-        ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
-                  QWidget *parent = nullptr);
-
-        ImageItem(const QString &url,
-                  const QString &filename,
-                  uint64_t size,
-                  QWidget *parent = nullptr);
-
-        QSize sizeHint() const override;
-
-public slots:
-        //! Show a save as dialog for the image.
-        void saveAs();
-        void setImage(const QPixmap &image);
-        void saveImage(const QString &filename, const QByteArray &data);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-
-        //! Whether the user can interact with the displayed image.
-        bool isInteractive_ = true;
-
-private:
-        void init();
-        void openUrl();
-        void downloadMedia(const QUrl &url);
-
-        int max_width_  = 500;
-        int max_height_ = 300;
-
-        int width_;
-        int height_;
-
-        QPixmap scaled_image_;
-        QPixmap image_;
-
-        QUrl url_;
-        QString text_;
-
-        int bottom_height_ = 30;
-
-        QRectF textRegion_;
-        QRectF imageRegion_;
-
-        mtx::events::RoomEvent<mtx::events::msg::Image> event_;
-};
-
-class StickerItem : public ImageItem
-{
-        Q_OBJECT
-
-public:
-        StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr)
-          : ImageItem{QString::fromStdString(event.content.url),
-                      QString::fromStdString(event.content.body),
-                      event.content.info.size,
-                      parent}
-          , event_{event}
-        {
-                isInteractive_ = false;
-                setCursor(Qt::ArrowCursor);
-                setMouseTracking(false);
-                setAttribute(Qt::WA_Hover, false);
-        }
-
-private:
-        mtx::events::Sticker event_;
-};
diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp
deleted file mode 100644
index 4b5dc02274b52110b1711cd437c2610554d18534..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/VideoItem.cpp
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QLabel>
-#include <QVBoxLayout>
-
-#include "Config.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-#include "timeline/widgets/VideoItem.h"
-
-void
-VideoItem::init()
-{
-        url_ = utils::mxcToHttp(
-          url_, QString::fromStdString(http::client()->server()), http::client()->port());
-}
-
-VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)
-  : QWidget(parent)
-  , url_{QString::fromStdString(event.content.url)}
-  , text_{QString::fromStdString(event.content.body)}
-  , event_{event}
-{
-        readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
-
-        init();
-
-        auto layout = new QVBoxLayout(this);
-        layout->setMargin(0);
-        layout->setSpacing(0);
-
-        QString link = QString("<a href=%1>%2</a>").arg(url_.toString()).arg(text_);
-
-        label_ = new QLabel(link, this);
-        label_->setMargin(0);
-        label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
-        label_->setOpenExternalLinks(true);
-
-        layout->addWidget(label_);
-}
-
-VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        readableFileSize_ = utils::humanReadableFileSize(size);
-
-        init();
-}
diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h
deleted file mode 100644
index 26fa1c35ba6e152f34f26001041add81d3342862..0000000000000000000000000000000000000000
--- a/src/timeline/widgets/VideoItem.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QLabel>
-#include <QSharedPointer>
-#include <QUrl>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-class VideoItem : public QWidget
-{
-        Q_OBJECT
-
-public:
-        VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
-                  QWidget *parent = nullptr);
-
-        VideoItem(const QString &url,
-                  const QString &filename,
-                  uint64_t size,
-                  QWidget *parent = nullptr);
-
-private:
-        void init();
-
-        QUrl url_;
-        QString text_;
-        QString readableFileSize_;
-
-        QLabel *label_;
-
-        mtx::events::RoomEvent<mtx::events::msg::Video> event_;
-};
diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp
index 501a8968bd37a7ad78eafc35d2591840a57f066b..e4a90f81d9b7c36a4379f5b0d72929072bf31e03 100644
--- a/src/ui/Avatar.cpp
+++ b/src/ui/Avatar.cpp
@@ -101,7 +101,7 @@ Avatar::setIcon(const QIcon &icon)
 void
 Avatar::paintEvent(QPaintEvent *)
 {
-        bool rounded = QSettings().value("user/avatar/circles", true).toBool();
+        bool rounded = QSettings().value("user/avatar_circles", true).toBool();
 
         QPainter painter(this);
         painter.setRenderHint(QPainter::Antialiasing);