diff --git a/.ci/macos/notarize.sh b/.ci/macos/notarize.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ca8646be51e4c6d32d105b718f92ccbbc383dbf2
--- /dev/null
+++ b/.ci/macos/notarize.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+set -u
+
+# Modified version of script found at:
+# https://forum.qt.io/topic/96652/how-to-notarize-qt-application-on-macos/18
+
+# Add Qt binaries to path
+PATH="/usr/local/opt/qt@5/bin/:${PATH}"
+
+security unlock-keychain -p "${RUNNER_USER_PW}" login.keychain
+
+( cd build || exit
+  # macdeployqt does not copy symlinks over.
+  # this specifically addresses icu4c issues but nothing else.
+  # We might not even need this any longer... 
+  # ICU_LIB="$(brew --prefix icu4c)/lib"
+  # export ICU_LIB
+  # mkdir -p nheko.app/Contents/Frameworks
+  # find "${ICU_LIB}" -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true
+
+  macdeployqt nheko.app -dmg -always-overwrite -qmldir=../resources/qml/ -sign-for-notarization="${APPLE_DEV_IDENTITY}"
+
+  user=$(id -nu)
+  chown "${user}" nheko.dmg
+)
+
+NOTARIZE_SUBMIT_LOG=$(mktemp -t notarize-submit)
+NOTARIZE_STATUS_LOG=$(mktemp -t notarize-status)
+
+finish() {
+  rm "$NOTARIZE_SUBMIT_LOG" "$NOTARIZE_STATUS_LOG"
+}
+trap finish EXIT
+
+dmgbuild -s .ci/macos/settings.json "Nheko" nheko.dmg
+codesign -s "${APPLE_DEV_IDENTITY}" nheko.dmg
+user=$(id -nu)
+chown "${user}" nheko.dmg
+
+echo "--> Start Notarization process"
+xcrun altool -t osx -f nheko.dmg --primary-bundle-id "io.github.nheko-reborn.nheko" --notarize-app -u "${APPLE_DEV_USER}" -p "${APPLE_DEV_PASS}" > "$NOTARIZE_SUBMIT_LOG" 2>&1
+requestUUID="$(awk -F ' = ' '/RequestUUID/ {print $2}' "$NOTARIZE_SUBMIT_LOG")"
+
+while sleep 60 && date; do
+  echo "--> Checking notarization status for ${requestUUID}"
+
+  xcrun altool --notarization-info "${requestUUID}" -u "${APPLE_DEV_USER}" -p "${APPLE_DEV_PASS}" > "$NOTARIZE_STATUS_LOG" 2>&1
+
+  isSuccess=$(grep "success" "$NOTARIZE_STATUS_LOG")
+  isFailure=$(grep "invalid" "$NOTARIZE_STATUS_LOG")
+
+  if [ -n "${isSuccess}" ]; then
+      echo "Notarization done!"
+      xcrun stapler staple -v nheko.dmg
+      echo "Stapler done!"
+      break
+  fi
+  if [ -n "${isFailure}" ]; then
+      echo "Notarization failed"
+      cat "$NOTARIZE_STATUS_LOG" 1>&2
+      return 1
+  fi
+  echo "Notarization not finished yet, sleep 1m then check again..."
+done
+
+VERSION=${CI_COMMIT_SHORT_SHA}
+
+if [ -n "$VERSION" ]; then
+    mv nheko.dmg "nheko-${VERSION}.dmg"
+    mkdir artifacts
+    cp "nheko-${VERSION}.dmg" artifacts/
+fi
\ No newline at end of file
diff --git a/.clang-format b/.clang-format
index 059aee19544defe1b39d89162cde8d48a7dc8936..b5e2f017e093ed1d1396484aa0c98d34ed6c3c07 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,14 +1,14 @@
 ---
 Language: Cpp
-Standard: Cpp11
-AccessModifierOffset: -8
+Standard: c++17
+AccessModifierOffset: -4
 AlignAfterOpenBracket: Align
 AlignConsecutiveAssignments: true
 AllowShortFunctionsOnASingleLine: true
 BasedOnStyle: Mozilla
 ColumnLimit: 100
 IndentCaseLabels: false
-IndentWidth: 8
+IndentWidth: 4
 KeepEmptyLinesAtTheStartOfBlocks: false
 PointerAlignment: Right
 Cpp11BracedListStyle: true
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cea6be7b5ee3e6cb009f5a3feb6763551cc0e8c2..1012c6900635c7f7fb4fe2e528a9f702cdad30cf 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -55,7 +55,6 @@ build-macos:
     #- brew update
     #- brew reinstall --force python3
     #- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
-    - pip3 install dmgbuild
     - rm -rf ../.hunter &&  mv .hunter ../.hunter || true
   script:
     - export PATH=/usr/local/opt/qt@5/bin/:${PATH}
@@ -72,19 +71,40 @@ build-macos:
     - cmake --build build
   after_script:
     - mv ../.hunter .hunter
-    - ./.ci/macos/deploy.sh
-    - ./.ci/upload-nightly-gitlab.sh artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
   artifacts:
     paths:
-      - artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
-    name: nheko-${CI_COMMIT_SHORT_SHA}-macos
-    expose_as: 'macos-dmg'
+      - build/nheko.app
+    name: nheko-${CI_COMMIT_SHORT_SHA}-macos-app
+    expose_as: 'macos-app'
+    public: false
   cache:
     key: "${CI_JOB_NAME}"
     paths:
       - .hunter/
       - "${CCACHE_DIR}"
 
+codesign-macos:
+  stage: deploy
+  tags: [macos]
+  before_script:
+    - pip3 install dmgbuild
+  script:
+    - export PATH=/usr/local/opt/qt@5/bin/:${PATH}
+    - ./.ci/macos/notarize.sh
+  after_script:
+    - ./.ci/upload-nightly-gitlab.sh artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
+  needs:
+    - build-macos
+  rules:
+    - if: '$CI_COMMIT_BRANCH == "master"'
+    - if : $CI_COMMIT_TAG
+  artifacts:
+    paths:
+      - artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
+    name: nheko-${CI_COMMIT_SHORT_SHA}-macos
+    expose_as: 'macos-dmg'
+
+
 build-flatpak-amd64:
   stage: build
   image: ubuntu:latest
@@ -171,7 +191,7 @@ appimage-amd64:
     - apt-get install -y git wget curl
 
     # update appimage-builder (optional)
-    - pip3 install --upgrade git+https://www.opencode.net/azubieta/appimagecraft.git
+    - pip3 install --upgrade git+https://github.com/AppImageCrafters/appimage-builder.git
 
     - apt-get update && apt-get -y install --no-install-recommends g++-7 build-essential ninja-build qt${QT_PKG}{base,declarative,tools,multimedia,script,quickcontrols2,svg} liblmdb-dev libssl-dev git ninja-build qt5keychain-dev libgtest-dev ccache libevent-dev libcurl4-openssl-dev libgl1-mesa-dev 
     - wget https://github.com/Kitware/CMake/releases/download/v3.19.0/cmake-3.19.0-Linux-x86_64.sh && sh cmake-3.19.0-Linux-x86_64.sh  --skip-license  --prefix=/usr/local
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8ef4470c8faf5909dcb69fe80e785ceef666ee80..fdbdaaa2c7c5326b347d1fbc27dd9f74c4aa81ad 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -281,8 +281,6 @@ set(SRC_FILES
 	src/dialogs/CreateRoom.cpp
 	src/dialogs/FallbackAuth.cpp
 	src/dialogs/ImageOverlay.cpp
-	src/dialogs/JoinRoom.cpp
-	src/dialogs/LeaveRoom.cpp
 	src/dialogs/Logout.cpp
 	src/dialogs/PreviewUploadOverlay.cpp
 	src/dialogs/ReCaptcha.cpp
@@ -311,6 +309,8 @@ set(SRC_FILES
 	src/ui/InfoMessage.cpp
 	src/ui/Label.cpp
 	src/ui/LoadingIndicator.cpp
+	src/ui/MxcAnimatedImage.cpp
+	src/ui/MxcMediaProxy.cpp
 	src/ui/NhekoCursorShape.cpp
 	src/ui/NhekoDropArea.cpp
 	src/ui/NhekoGlobalObject.cpp
@@ -326,31 +326,38 @@ set(SRC_FILES
 	src/ui/Theme.cpp
 	src/ui/ThemeManager.cpp
 	src/ui/ToggleButton.cpp
+	src/ui/UIA.cpp
 	src/ui/UserProfile.cpp
 
+	src/voip/CallDevices.cpp
+	src/voip/CallManager.cpp
+	src/voip/WebRTCSession.cpp
+
+	src/encryption/DeviceVerificationFlow.cpp
+	src/encryption/Olm.cpp
+	src/encryption/SelfVerificationStatus.cpp
+	src/encryption/VerificationManager.cpp
+
 	# Generic notification stuff
 	src/notifications/Manager.cpp
 
 	src/AvatarProvider.cpp
 	src/BlurhashProvider.cpp
 	src/Cache.cpp
-	src/CallDevices.cpp
-	src/CallManager.cpp
 	src/ChatPage.cpp
 	src/Clipboard.cpp
 	src/ColorImageProvider.cpp
 	src/CompletionProxyModel.cpp
-	src/DeviceVerificationFlow.cpp
 	src/EventAccessors.cpp
 	src/InviteesModel.cpp
+	src/JdenticonProvider.cpp
 	src/Logging.cpp
 	src/LoginPage.cpp
 	src/MainWindow.cpp
 	src/MatrixClient.cpp
 	src/MemberList.cpp
 	src/MxcImageProvider.cpp
-	src/Olm.cpp
-    src/ReadReceiptsModel.cpp
+	src/ReadReceiptsModel.cpp
 	src/RegisterPage.cpp
 	src/SSOHandler.cpp
 	src/CombinedImagePackModel.cpp
@@ -359,9 +366,9 @@ set(SRC_FILES
 	src/TrayIcon.cpp
 	src/UserSettingsPage.cpp
 	src/UsersModel.cpp
+	src/RoomDirectoryModel.cpp
 	src/RoomsModel.cpp
 	src/Utils.cpp
-	src/WebRTCSession.cpp
 	src/WelcomePage.cpp
 	src/main.cpp
 
@@ -381,7 +388,7 @@ if(USE_BUNDLED_MTXCLIENT)
 	FetchContent_Declare(
 		MatrixClient
 		GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-		GIT_TAG        deb51ef1d6df870098069312f0a1999550e1eb85
+		GIT_TAG        7fe7a70fcf7540beb6d7b4847e53a425de66c6bf
 		)
 	set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
 	set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@@ -492,8 +499,6 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/dialogs/CreateRoom.h
 	src/dialogs/FallbackAuth.h
 	src/dialogs/ImageOverlay.h
-	src/dialogs/JoinRoom.h
-	src/dialogs/LeaveRoom.h
 	src/dialogs/Logout.h
 	src/dialogs/PreviewUploadOverlay.h
 	src/dialogs/ReCaptcha.h
@@ -520,6 +525,8 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/ui/InfoMessage.h
 	src/ui/Label.h
 	src/ui/LoadingIndicator.h
+	src/ui/MxcAnimatedImage.h
+	src/ui/MxcMediaProxy.h
 	src/ui/Menu.h
 	src/ui/NhekoCursorShape.h
 	src/ui/NhekoDropArea.h
@@ -535,28 +542,35 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/ui/Theme.h
 	src/ui/ThemeManager.h
 	src/ui/ToggleButton.h
+	src/ui/UIA.h
 	src/ui/UserProfile.h
 
+	src/voip/CallDevices.h
+	src/voip/CallManager.h
+	src/voip/WebRTCSession.h
+
+	src/encryption/DeviceVerificationFlow.h
+	src/encryption/Olm.h
+	src/encryption/SelfVerificationStatus.h
+	src/encryption/VerificationManager.h
+
 	src/notifications/Manager.h
 
 	src/AvatarProvider.h
 	src/BlurhashProvider.h
 	src/CacheCryptoStructs.h
 	src/Cache_p.h
-	src/CallDevices.h
-	src/CallManager.h
 	src/ChatPage.h
 	src/Clipboard.h
 	src/CombinedImagePackModel.h
 	src/CompletionProxyModel.h
-	src/DeviceVerificationFlow.h
 	src/ImagePackListModel.h
 	src/InviteesModel.h
+	src/JdenticonProvider.h
 	src/LoginPage.h
 	src/MainWindow.h
 	src/MemberList.h
 	src/MxcImageProvider.h
-	src/Olm.h
 	src/RegisterPage.h
 	src/RoomsModel.h
 	src/SSOHandler.h
@@ -564,7 +578,8 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/TrayIcon.h
 	src/UserSettingsPage.h
 	src/UsersModel.h
-	src/WebRTCSession.h
+	src/RoomDirectoryModel.h
+	src/RoomsModel.h
 	src/WelcomePage.h
 	src/ReadReceiptsModel.h
 )
@@ -576,7 +591,7 @@ include(Translations)
 set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
 
 if (APPLE)
-	set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa")
+	set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa -framework UserNotifications")
 	set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/notifications/ManagerMac.cpp src/emoji/MacHelper.mm)
 	if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
 		set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
diff --git a/README.md b/README.md
index 1cf5d70565fdea2dae4e0ef10b2798d7c63cbd71..b5ed4966bc3a936aeee6372d462329f0c83f70e7 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,7 @@ sudo dnf install nheko
 #### Gentoo Linux
 ```bash
 sudo eselect repository enable guru
+sudo emaint sync -r guru
 sudo emerge -a nheko
 ```
 
@@ -126,11 +127,31 @@ choco install nheko-reborn
 
 ### FAQ
 
-##
+---
+
 **Q:** Why don't videos run for me on Windows?
 
 **A:** You're probably missing the required video codecs, download [K-Lite Codec Pack](https://codecguide.com/download_kl.htm).
-##
+
+---
+
+**Q:** What commands are supported by nheko?
+
+**A:** See <https://github.com/Nheko-Reborn/nheko/wiki/Commands>
+
+---
+
+**Q:** Does nheko support end-to-end encryption (EE2E)?
+
+**A:** Yes, see [feature list](#features)
+
+---
+
+**Q:** Can I test a bleeding edge development version?
+
+**A:** Checkout nightly builds <https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/>
+
+---
 
 ### Build Requirements
 
@@ -150,7 +171,7 @@ choco install nheko-reborn
     - Voice call support: dtls, opus, rtpmanager, srtp, webrtc
     - Video call support (optional): compositor, opengl, qmlgl, rtp, vpx
     - [libnice](https://gitlab.freedesktop.org/libnice/libnice)
-- [qtkeychain](https://github.com/frankosterfeld/qtkeychain)
+- [qtkeychain](https://github.com/frankosterfeld/qtkeychain) (You need at least version 0.12 for proper Gnome Keychain support)
 - A compiler that supports C++ 17:
     - Clang 6 (tested on Travis CI)
     - GCC 7 (tested on Travis CI)
diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml
index c9caddc86b617e940b10b9b8735b7547830f7984..19a2ad4f6d63629fb3c7542a1bdf28be09044a7a 100644
--- a/io.github.NhekoReborn.Nheko.yaml
+++ b/io.github.NhekoReborn.Nheko.yaml
@@ -163,7 +163,7 @@ modules:
     buildsystem: cmake-ninja
     name: mtxclient
     sources:
-      - commit: deb51ef1d6df870098069312f0a1999550e1eb85
+      - commit: 7fe7a70fcf7540beb6d7b4847e53a425de66c6bf
         type: git
         url: https://github.com/Nheko-Reborn/mtxclient.git
   - config-opts:
diff --git a/resources/emoji-test.txt b/resources/emoji-test.txt
index d3c6d12bd5905ef3244844f6dd510637bfb3568c..dd54933661c85c7337a71a35eee93e9094ba2fe2 100644
--- a/resources/emoji-test.txt
+++ b/resources/emoji-test.txt
@@ -1,11 +1,11 @@
 # emoji-test.txt
-# Date: 2020-09-12, 22:19:50 GMT
-# © 2020 Unicode®, Inc.
+# Date: 2021-08-26, 17:22:23 GMT
+# © 2021 Unicode®, Inc.
 # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
 # For terms of use, see http://www.unicode.org/terms_of_use.html
 #
 # Emoji Keyboard/Display Test Data for UTS #51
-# Version: 13.1
+# Version: 14.0
 #
 # For documentation and usage, see http://www.unicode.org/reports/tr51
 #
@@ -43,6 +43,7 @@
 1F602                                                  ; fully-qualified     # 😂 E0.6 face with tears of joy
 1F642                                                  ; fully-qualified     # 🙂 E1.0 slightly smiling face
 1F643                                                  ; fully-qualified     # 🙃 E1.0 upside-down face
+1FAE0                                                  ; fully-qualified     # 🫠 E14.0 melting face
 1F609                                                  ; fully-qualified     # 😉 E0.6 winking face
 1F60A                                                  ; fully-qualified     # 😊 E0.6 smiling face with smiling eyes
 1F607                                                  ; fully-qualified     # 😇 E1.0 smiling face with halo
@@ -68,10 +69,13 @@
 1F911                                                  ; fully-qualified     # 🤑 E1.0 money-mouth face
 
 # subgroup: face-hand
-1F917                                                  ; fully-qualified     # 🤗 E1.0 hugging face
+1F917                                                  ; fully-qualified     # 🤗 E1.0 smiling face with open hands
 1F92D                                                  ; fully-qualified     # 🤭 E5.0 face with hand over mouth
+1FAE2                                                  ; fully-qualified     # 🫢 E14.0 face with open eyes and hand over mouth
+1FAE3                                                  ; fully-qualified     # 🫣 E14.0 face with peeking eye
 1F92B                                                  ; fully-qualified     # 🤫 E5.0 shushing face
 1F914                                                  ; fully-qualified     # 🤔 E1.0 thinking face
+1FAE1                                                  ; fully-qualified     # 🫡 E14.0 saluting face
 
 # subgroup: face-neutral-skeptical
 1F910                                                  ; fully-qualified     # 🤐 E1.0 zipper-mouth face
@@ -79,6 +83,7 @@
 1F610                                                  ; fully-qualified     # 😐 E0.7 neutral face
 1F611                                                  ; fully-qualified     # 😑 E1.0 expressionless face
 1F636                                                  ; fully-qualified     # 😶 E1.0 face without mouth
+1FAE5                                                  ; fully-qualified     # 🫥 E14.0 dotted line face
 1F636 200D 1F32B FE0F                                  ; fully-qualified     # 😶‍🌫️ E13.1 face in clouds
 1F636 200D 1F32B                                       ; minimally-qualified # 😶‍🌫 E13.1 face in clouds
 1F60F                                                  ; fully-qualified     # 😏 E0.6 smirking face
@@ -105,7 +110,7 @@
 1F975                                                  ; fully-qualified     # 🥵 E11.0 hot face
 1F976                                                  ; fully-qualified     # 🥶 E11.0 cold face
 1F974                                                  ; fully-qualified     # 🥴 E11.0 woozy face
-1F635                                                  ; fully-qualified     # 😵 E0.6 knocked-out face
+1F635                                                  ; fully-qualified     # 😵 E0.6 face with crossed-out eyes
 1F635 200D 1F4AB                                       ; fully-qualified     # 😵‍💫 E13.1 face with spiral eyes
 1F92F                                                  ; fully-qualified     # 🤯 E5.0 exploding head
 
@@ -121,6 +126,7 @@
 
 # subgroup: face-concerned
 1F615                                                  ; fully-qualified     # 😕 E1.0 confused face
+1FAE4                                                  ; fully-qualified     # 🫤 E14.0 face with diagonal mouth
 1F61F                                                  ; fully-qualified     # 😟 E1.0 worried face
 1F641                                                  ; fully-qualified     # 🙁 E1.0 slightly frowning face
 2639 FE0F                                              ; fully-qualified     # ☹️ E0.7 frowning face
@@ -130,6 +136,7 @@
 1F632                                                  ; fully-qualified     # 😲 E0.6 astonished face
 1F633                                                  ; fully-qualified     # 😳 E0.6 flushed face
 1F97A                                                  ; fully-qualified     # 🥺 E11.0 pleading face
+1F979                                                  ; fully-qualified     # 🥹 E14.0 face holding back tears
 1F626                                                  ; fully-qualified     # 😦 E1.0 frowning face with open mouth
 1F627                                                  ; fully-qualified     # 😧 E1.0 anguished face
 1F628                                                  ; fully-qualified     # 😨 E0.6 fearful face
@@ -232,8 +239,8 @@
 1F4AD                                                  ; fully-qualified     # 💭 E1.0 thought balloon
 1F4A4                                                  ; fully-qualified     # 💤 E0.6 zzz
 
-# Smileys & Emotion subtotal:		170
-# Smileys & Emotion subtotal:		170	w/o modifiers
+# Smileys & Emotion subtotal:		177
+# Smileys & Emotion subtotal:		177	w/o modifiers
 
 # group: People & Body
 
@@ -269,6 +276,30 @@
 1F596 1F3FD                                            ; fully-qualified     # 🖖🏽 E1.0 vulcan salute: medium skin tone
 1F596 1F3FE                                            ; fully-qualified     # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
 1F596 1F3FF                                            ; fully-qualified     # 🖖🏿 E1.0 vulcan salute: dark skin tone
+1FAF1                                                  ; fully-qualified     # 🫱 E14.0 rightwards hand
+1FAF1 1F3FB                                            ; fully-qualified     # 🫱🏻 E14.0 rightwards hand: light skin tone
+1FAF1 1F3FC                                            ; fully-qualified     # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
+1FAF1 1F3FD                                            ; fully-qualified     # 🫱🏽 E14.0 rightwards hand: medium skin tone
+1FAF1 1F3FE                                            ; fully-qualified     # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
+1FAF1 1F3FF                                            ; fully-qualified     # 🫱🏿 E14.0 rightwards hand: dark skin tone
+1FAF2                                                  ; fully-qualified     # 🫲 E14.0 leftwards hand
+1FAF2 1F3FB                                            ; fully-qualified     # 🫲🏻 E14.0 leftwards hand: light skin tone
+1FAF2 1F3FC                                            ; fully-qualified     # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
+1FAF2 1F3FD                                            ; fully-qualified     # 🫲🏽 E14.0 leftwards hand: medium skin tone
+1FAF2 1F3FE                                            ; fully-qualified     # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
+1FAF2 1F3FF                                            ; fully-qualified     # 🫲🏿 E14.0 leftwards hand: dark skin tone
+1FAF3                                                  ; fully-qualified     # 🫳 E14.0 palm down hand
+1FAF3 1F3FB                                            ; fully-qualified     # 🫳🏻 E14.0 palm down hand: light skin tone
+1FAF3 1F3FC                                            ; fully-qualified     # 🫳🏼 E14.0 palm down hand: medium-light skin tone
+1FAF3 1F3FD                                            ; fully-qualified     # 🫳🏽 E14.0 palm down hand: medium skin tone
+1FAF3 1F3FE                                            ; fully-qualified     # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
+1FAF3 1F3FF                                            ; fully-qualified     # 🫳🏿 E14.0 palm down hand: dark skin tone
+1FAF4                                                  ; fully-qualified     # 🫴 E14.0 palm up hand
+1FAF4 1F3FB                                            ; fully-qualified     # 🫴🏻 E14.0 palm up hand: light skin tone
+1FAF4 1F3FC                                            ; fully-qualified     # 🫴🏼 E14.0 palm up hand: medium-light skin tone
+1FAF4 1F3FD                                            ; fully-qualified     # 🫴🏽 E14.0 palm up hand: medium skin tone
+1FAF4 1F3FE                                            ; fully-qualified     # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
+1FAF4 1F3FF                                            ; fully-qualified     # 🫴🏿 E14.0 palm up hand: dark skin tone
 
 # subgroup: hand-fingers-partial
 1F44C                                                  ; fully-qualified     # 👌 E0.6 OK hand
@@ -302,6 +333,12 @@
 1F91E 1F3FD                                            ; fully-qualified     # 🤞🏽 E3.0 crossed fingers: medium skin tone
 1F91E 1F3FE                                            ; fully-qualified     # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
 1F91E 1F3FF                                            ; fully-qualified     # 🤞🏿 E3.0 crossed fingers: dark skin tone
+1FAF0                                                  ; fully-qualified     # 🫰 E14.0 hand with index finger and thumb crossed
+1FAF0 1F3FB                                            ; fully-qualified     # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
+1FAF0 1F3FC                                            ; fully-qualified     # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
+1FAF0 1F3FD                                            ; fully-qualified     # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
+1FAF0 1F3FE                                            ; fully-qualified     # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
+1FAF0 1F3FF                                            ; fully-qualified     # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
 1F91F                                                  ; fully-qualified     # 🤟 E5.0 love-you gesture
 1F91F 1F3FB                                            ; fully-qualified     # 🤟🏻 E5.0 love-you gesture: light skin tone
 1F91F 1F3FC                                            ; fully-qualified     # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
@@ -359,6 +396,12 @@
 261D 1F3FD                                             ; fully-qualified     # ☝🏽 E1.0 index pointing up: medium skin tone
 261D 1F3FE                                             ; fully-qualified     # ☝🏾 E1.0 index pointing up: medium-dark skin tone
 261D 1F3FF                                             ; fully-qualified     # ☝🏿 E1.0 index pointing up: dark skin tone
+1FAF5                                                  ; fully-qualified     # 🫵 E14.0 index pointing at the viewer
+1FAF5 1F3FB                                            ; fully-qualified     # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
+1FAF5 1F3FC                                            ; fully-qualified     # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
+1FAF5 1F3FD                                            ; fully-qualified     # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
+1FAF5 1F3FE                                            ; fully-qualified     # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
+1FAF5 1F3FF                                            ; fully-qualified     # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
 
 # subgroup: hand-fingers-closed
 1F44D                                                  ; fully-qualified     # 👍 E0.6 thumbs up
@@ -411,6 +454,12 @@
 1F64C 1F3FD                                            ; fully-qualified     # 🙌🏽 E1.0 raising hands: medium skin tone
 1F64C 1F3FE                                            ; fully-qualified     # 🙌🏾 E1.0 raising hands: medium-dark skin tone
 1F64C 1F3FF                                            ; fully-qualified     # 🙌🏿 E1.0 raising hands: dark skin tone
+1FAF6                                                  ; fully-qualified     # 🫶 E14.0 heart hands
+1FAF6 1F3FB                                            ; fully-qualified     # 🫶🏻 E14.0 heart hands: light skin tone
+1FAF6 1F3FC                                            ; fully-qualified     # 🫶🏼 E14.0 heart hands: medium-light skin tone
+1FAF6 1F3FD                                            ; fully-qualified     # 🫶🏽 E14.0 heart hands: medium skin tone
+1FAF6 1F3FE                                            ; fully-qualified     # 🫶🏾 E14.0 heart hands: medium-dark skin tone
+1FAF6 1F3FF                                            ; fully-qualified     # 🫶🏿 E14.0 heart hands: dark skin tone
 1F450                                                  ; fully-qualified     # 👐 E0.6 open hands
 1F450 1F3FB                                            ; fully-qualified     # 👐🏻 E1.0 open hands: light skin tone
 1F450 1F3FC                                            ; fully-qualified     # 👐🏼 E1.0 open hands: medium-light skin tone
@@ -424,6 +473,31 @@
 1F932 1F3FE                                            ; fully-qualified     # 🤲🏾 E5.0 palms up together: medium-dark skin tone
 1F932 1F3FF                                            ; fully-qualified     # 🤲🏿 E5.0 palms up together: dark skin tone
 1F91D                                                  ; fully-qualified     # 🤝 E3.0 handshake
+1F91D 1F3FB                                            ; fully-qualified     # 🤝🏻 E3.0 handshake: light skin tone
+1F91D 1F3FC                                            ; fully-qualified     # 🤝🏼 E3.0 handshake: medium-light skin tone
+1F91D 1F3FD                                            ; fully-qualified     # 🤝🏽 E3.0 handshake: medium skin tone
+1F91D 1F3FE                                            ; fully-qualified     # 🤝🏾 E3.0 handshake: medium-dark skin tone
+1F91D 1F3FF                                            ; fully-qualified     # 🤝🏿 E3.0 handshake: dark skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏻‍🫲🏿 E14.0 handshake: light skin tone, dark skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏼‍🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏼‍🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏼‍🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏼‍🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏽‍🫲🏻 E14.0 handshake: medium skin tone, light skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏽‍🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏽‍🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏽‍🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏾‍🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏾‍🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏾‍🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏾‍🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏿‍🫲🏻 E14.0 handshake: dark skin tone, light skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏿‍🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏿‍🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏿‍🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
 1F64F                                                  ; fully-qualified     # 🙏 E0.6 folded hands
 1F64F 1F3FB                                            ; fully-qualified     # 🙏🏻 E1.0 folded hands: light skin tone
 1F64F 1F3FC                                            ; fully-qualified     # 🙏🏼 E1.0 folded hands: medium-light skin tone
@@ -501,6 +575,7 @@
 1F441                                                  ; unqualified         # 👁 E0.7 eye
 1F445                                                  ; fully-qualified     # 👅 E0.6 tongue
 1F444                                                  ; fully-qualified     # 👄 E0.6 mouth
+1FAE6                                                  ; fully-qualified     # 🫦 E14.0 biting lip
 
 # subgroup: person
 1F476                                                  ; fully-qualified     # 👶 E0.6 baby
@@ -1472,6 +1547,12 @@
 1F477 1F3FE 200D 2640                                  ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone
 1F477 1F3FF 200D 2640 FE0F                             ; fully-qualified     # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone
 1F477 1F3FF 200D 2640                                  ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone
+1FAC5                                                  ; fully-qualified     # 🫅 E14.0 person with crown
+1FAC5 1F3FB                                            ; fully-qualified     # 🫅🏻 E14.0 person with crown: light skin tone
+1FAC5 1F3FC                                            ; fully-qualified     # 🫅🏼 E14.0 person with crown: medium-light skin tone
+1FAC5 1F3FD                                            ; fully-qualified     # 🫅🏽 E14.0 person with crown: medium skin tone
+1FAC5 1F3FE                                            ; fully-qualified     # 🫅🏾 E14.0 person with crown: medium-dark skin tone
+1FAC5 1F3FF                                            ; fully-qualified     # 🫅🏿 E14.0 person with crown: dark skin tone
 1F934                                                  ; fully-qualified     # 🤴 E3.0 prince
 1F934 1F3FB                                            ; fully-qualified     # 🤴🏻 E3.0 prince: light skin tone
 1F934 1F3FC                                            ; fully-qualified     # 🤴🏼 E3.0 prince: medium-light skin tone
@@ -1592,6 +1673,18 @@
 1F930 1F3FD                                            ; fully-qualified     # 🤰🏽 E3.0 pregnant woman: medium skin tone
 1F930 1F3FE                                            ; fully-qualified     # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
 1F930 1F3FF                                            ; fully-qualified     # 🤰🏿 E3.0 pregnant woman: dark skin tone
+1FAC3                                                  ; fully-qualified     # 🫃 E14.0 pregnant man
+1FAC3 1F3FB                                            ; fully-qualified     # 🫃🏻 E14.0 pregnant man: light skin tone
+1FAC3 1F3FC                                            ; fully-qualified     # 🫃🏼 E14.0 pregnant man: medium-light skin tone
+1FAC3 1F3FD                                            ; fully-qualified     # 🫃🏽 E14.0 pregnant man: medium skin tone
+1FAC3 1F3FE                                            ; fully-qualified     # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
+1FAC3 1F3FF                                            ; fully-qualified     # 🫃🏿 E14.0 pregnant man: dark skin tone
+1FAC4                                                  ; fully-qualified     # 🫄 E14.0 pregnant person
+1FAC4 1F3FB                                            ; fully-qualified     # 🫄🏻 E14.0 pregnant person: light skin tone
+1FAC4 1F3FC                                            ; fully-qualified     # 🫄🏼 E14.0 pregnant person: medium-light skin tone
+1FAC4 1F3FD                                            ; fully-qualified     # 🫄🏽 E14.0 pregnant person: medium skin tone
+1FAC4 1F3FE                                            ; fully-qualified     # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
+1FAC4 1F3FF                                            ; fully-qualified     # 🫄🏿 E14.0 pregnant person: dark skin tone
 1F931                                                  ; fully-qualified     # 🤱 E5.0 breast-feeding
 1F931 1F3FB                                            ; fully-qualified     # 🤱🏻 E5.0 breast-feeding: light skin tone
 1F931 1F3FC                                            ; fully-qualified     # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
@@ -1862,6 +1955,7 @@
 1F9DF 200D 2642                                        ; minimally-qualified # 🧟‍♂ E5.0 man zombie
 1F9DF 200D 2640 FE0F                                   ; fully-qualified     # 🧟‍♀️ E5.0 woman zombie
 1F9DF 200D 2640                                        ; minimally-qualified # 🧟‍♀ E5.0 woman zombie
+1F9CC                                                  ; fully-qualified     # 🧌 E14.0 troll
 
 # subgroup: person-activity
 1F486                                                  ; fully-qualified     # 💆 E0.6 person getting massage
@@ -3168,8 +3262,8 @@
 1FAC2                                                  ; fully-qualified     # 🫂 E13.0 people hugging
 1F463                                                  ; fully-qualified     # 👣 E0.6 footprints
 
-# People & Body subtotal:		2899
-# People & Body subtotal:		494	w/o modifiers
+# People & Body subtotal:		2986
+# People & Body subtotal:		506	w/o modifiers
 
 # group: Component
 
@@ -3304,6 +3398,7 @@
 1F988                                                  ; fully-qualified     # 🦈 E3.0 shark
 1F419                                                  ; fully-qualified     # 🐙 E0.6 octopus
 1F41A                                                  ; fully-qualified     # 🐚 E0.6 spiral shell
+1FAB8                                                  ; fully-qualified     # 🪸 E14.0 coral
 
 # subgroup: animal-bug
 1F40C                                                  ; fully-qualified     # 🐌 E0.6 snail
@@ -3329,6 +3424,7 @@
 1F490                                                  ; fully-qualified     # 💐 E0.6 bouquet
 1F338                                                  ; fully-qualified     # 🌸 E0.6 cherry blossom
 1F4AE                                                  ; fully-qualified     # 💮 E0.6 white flower
+1FAB7                                                  ; fully-qualified     # 🪷 E14.0 lotus
 1F3F5 FE0F                                             ; fully-qualified     # 🏵️ E0.7 rosette
 1F3F5                                                  ; unqualified         # 🏵 E0.7 rosette
 1F339                                                  ; fully-qualified     # 🌹 E0.6 rose
@@ -3353,9 +3449,11 @@
 1F341                                                  ; fully-qualified     # 🍁 E0.6 maple leaf
 1F342                                                  ; fully-qualified     # 🍂 E0.6 fallen leaf
 1F343                                                  ; fully-qualified     # 🍃 E0.6 leaf fluttering in wind
+1FAB9                                                  ; fully-qualified     # 🪹 E14.0 empty nest
+1FABA                                                  ; fully-qualified     # 🪺 E14.0 nest with eggs
 
-# Animals & Nature subtotal:		147
-# Animals & Nature subtotal:		147	w/o modifiers
+# Animals & Nature subtotal:		151
+# Animals & Nature subtotal:		151	w/o modifiers
 
 # group: Food & Drink
 
@@ -3396,6 +3494,7 @@
 1F9C5                                                  ; fully-qualified     # 🧅 E12.0 onion
 1F344                                                  ; fully-qualified     # 🍄 E0.6 mushroom
 1F95C                                                  ; fully-qualified     # 🥜 E3.0 peanuts
+1FAD8                                                  ; fully-qualified     # 🫘 E14.0 beans
 1F330                                                  ; fully-qualified     # 🌰 E0.6 chestnut
 
 # subgroup: food-prepared
@@ -3491,6 +3590,7 @@
 1F37B                                                  ; fully-qualified     # 🍻 E0.6 clinking beer mugs
 1F942                                                  ; fully-qualified     # 🥂 E3.0 clinking glasses
 1F943                                                  ; fully-qualified     # 🥃 E3.0 tumbler glass
+1FAD7                                                  ; fully-qualified     # 🫗 E14.0 pouring liquid
 1F964                                                  ; fully-qualified     # 🥤 E5.0 cup with straw
 1F9CB                                                  ; fully-qualified     # 🧋 E13.0 bubble tea
 1F9C3                                                  ; fully-qualified     # 🧃 E12.0 beverage box
@@ -3504,10 +3604,11 @@
 1F374                                                  ; fully-qualified     # 🍴 E0.6 fork and knife
 1F944                                                  ; fully-qualified     # 🥄 E3.0 spoon
 1F52A                                                  ; fully-qualified     # 🔪 E0.6 kitchen knife
+1FAD9                                                  ; fully-qualified     # 🫙 E14.0 jar
 1F3FA                                                  ; fully-qualified     # 🏺 E1.0 amphora
 
-# Food & Drink subtotal:		131
-# Food & Drink subtotal:		131	w/o modifiers
+# Food & Drink subtotal:		134
+# Food & Drink subtotal:		134	w/o modifiers
 
 # group: Travel & Places
 
@@ -3597,6 +3698,7 @@
 2668 FE0F                                              ; fully-qualified     # ♨️ E0.6 hot springs
 2668                                                   ; unqualified         # ♨ E0.6 hot springs
 1F3A0                                                  ; fully-qualified     # 🎠 E0.6 carousel horse
+1F6DD                                                  ; fully-qualified     # 🛝 E14.0 playground slide
 1F3A1                                                  ; fully-qualified     # 🎡 E0.6 ferris wheel
 1F3A2                                                  ; fully-qualified     # 🎢 E0.6 roller coaster
 1F488                                                  ; fully-qualified     # 💈 E0.6 barber pole
@@ -3652,6 +3754,7 @@
 1F6E2 FE0F                                             ; fully-qualified     # 🛢️ E0.7 oil drum
 1F6E2                                                  ; unqualified         # 🛢 E0.7 oil drum
 26FD                                                   ; fully-qualified     # ⛽ E0.6 fuel pump
+1F6DE                                                  ; fully-qualified     # 🛞 E14.0 wheel
 1F6A8                                                  ; fully-qualified     # 🚨 E0.6 police car light
 1F6A5                                                  ; fully-qualified     # 🚥 E0.6 horizontal traffic light
 1F6A6                                                  ; fully-qualified     # 🚦 E1.0 vertical traffic light
@@ -3660,6 +3763,7 @@
 
 # subgroup: transport-water
 2693                                                   ; fully-qualified     # âš“ E0.6 anchor
+1F6DF                                                  ; fully-qualified     # 🛟 E14.0 ring buoy
 26F5                                                   ; fully-qualified     # ⛵ E0.6 sailboat
 1F6F6                                                  ; fully-qualified     # 🛶 E3.0 canoe
 1F6A4                                                  ; fully-qualified     # 🚤 E0.6 speedboat
@@ -3797,8 +3901,8 @@
 1F4A7                                                  ; fully-qualified     # 💧 E0.6 droplet
 1F30A                                                  ; fully-qualified     # 🌊 E0.6 water wave
 
-# Travel & Places subtotal:		264
-# Travel & Places subtotal:		264	w/o modifiers
+# Travel & Places subtotal:		267
+# Travel & Places subtotal:		267	w/o modifiers
 
 # group: Activities
 
@@ -3874,6 +3978,7 @@
 1F52E                                                  ; fully-qualified     # 🔮 E0.6 crystal ball
 1FA84                                                  ; fully-qualified     # 🪄 E13.0 magic wand
 1F9FF                                                  ; fully-qualified     # 🧿 E11.0 nazar amulet
+1FAAC                                                  ; fully-qualified     # 🪬 E14.0 hamsa
 1F3AE                                                  ; fully-qualified     # 🎮 E0.6 video game
 1F579 FE0F                                             ; fully-qualified     # 🕹️ E0.7 joystick
 1F579                                                  ; unqualified         # 🕹 E0.7 joystick
@@ -3882,6 +3987,7 @@
 1F9E9                                                  ; fully-qualified     # 🧩 E11.0 puzzle piece
 1F9F8                                                  ; fully-qualified     # 🧸 E11.0 teddy bear
 1FA85                                                  ; fully-qualified     # 🪅 E13.0 piñata
+1FAA9                                                  ; fully-qualified     # 🪩 E14.0 mirror ball
 1FA86                                                  ; fully-qualified     # 🪆 E13.0 nesting dolls
 2660 FE0F                                              ; fully-qualified     # ♠️ E0.6 spade suit
 2660                                                   ; unqualified         # â™  E0.6 spade suit
@@ -3907,8 +4013,8 @@
 1F9F6                                                  ; fully-qualified     # 🧶 E11.0 yarn
 1FAA2                                                  ; fully-qualified     # 🪢 E13.0 knot
 
-# Activities subtotal:		95
-# Activities subtotal:		95	w/o modifiers
+# Activities subtotal:		97
+# Activities subtotal:		97	w/o modifiers
 
 # group: Objects
 
@@ -4009,6 +4115,7 @@
 
 # subgroup: computer
 1F50B                                                  ; fully-qualified     # 🔋 E0.6 battery
+1FAAB                                                  ; fully-qualified     # 🪫 E14.0 low battery
 1F50C                                                  ; fully-qualified     # 🔌 E0.6 electric plug
 1F4BB                                                  ; fully-qualified     # 💻 E0.6 laptop
 1F5A5 FE0F                                             ; fully-qualified     # 🖥️ E0.7 desktop computer
@@ -4207,7 +4314,9 @@
 1FA78                                                  ; fully-qualified     # 🩸 E12.0 drop of blood
 1F48A                                                  ; fully-qualified     # 💊 E0.6 pill
 1FA79                                                  ; fully-qualified     # 🩹 E12.0 adhesive bandage
+1FA7C                                                  ; fully-qualified     # 🩼 E14.0 crutch
 1FA7A                                                  ; fully-qualified     # 🩺 E12.0 stethoscope
+1FA7B                                                  ; fully-qualified     # 🩻 E14.0 x-ray
 
 # subgroup: household
 1F6AA                                                  ; fully-qualified     # 🚪 E0.6 door
@@ -4232,6 +4341,7 @@
 1F9FB                                                  ; fully-qualified     # 🧻 E11.0 roll of paper
 1FAA3                                                  ; fully-qualified     # 🪣 E13.0 bucket
 1F9FC                                                  ; fully-qualified     # 🧼 E11.0 soap
+1FAE7                                                  ; fully-qualified     # 🫧 E14.0 bubbles
 1FAA5                                                  ; fully-qualified     # 🪥 E13.0 toothbrush
 1F9FD                                                  ; fully-qualified     # 🧽 E11.0 sponge
 1F9EF                                                  ; fully-qualified     # 🧯 E11.0 fire extinguisher
@@ -4246,9 +4356,10 @@
 26B1                                                   ; unqualified         # âš± E1.0 funeral urn
 1F5FF                                                  ; fully-qualified     # 🗿 E0.6 moai
 1FAA7                                                  ; fully-qualified     # 🪧 E13.0 placard
+1FAAA                                                  ; fully-qualified     # 🪪 E14.0 identification card
 
-# Objects subtotal:		299
-# Objects subtotal:		299	w/o modifiers
+# Objects subtotal:		304
+# Objects subtotal:		304	w/o modifiers
 
 # group: Symbols
 
@@ -4409,6 +4520,7 @@
 2795                                                   ; fully-qualified     # âž• E0.6 plus
 2796                                                   ; fully-qualified     # âž– E0.6 minus
 2797                                                   ; fully-qualified     # âž— E0.6 divide
+1F7F0                                                  ; fully-qualified     # 🟰 E14.0 heavy equals sign
 267E FE0F                                              ; fully-qualified     # ♾️ E11.0 infinity
 267E                                                   ; unqualified         # ♾ E11.0 infinity
 
@@ -4581,8 +4693,8 @@
 1F533                                                  ; fully-qualified     # 🔳 E0.6 white square button
 1F532                                                  ; fully-qualified     # 🔲 E0.6 black square button
 
-# Symbols subtotal:		301
-# Symbols subtotal:		301	w/o modifiers
+# Symbols subtotal:		302
+# Symbols subtotal:		302	w/o modifiers
 
 # group: Flags
 
@@ -4871,7 +4983,7 @@
 # Flags subtotal:		275	w/o modifiers
 
 # Status Counts
-# fully-qualified : 3512
+# fully-qualified : 3624
 # minimally-qualified : 817
 # unqualified : 252
 # component : 9
diff --git a/resources/icons/ui/refresh.png b/resources/icons/ui/refresh.png
new file mode 100644
index 0000000000000000000000000000000000000000..642682032ab6d6ff73b0769bd2c420e89281d8a9
--- /dev/null
+++ b/resources/icons/ui/refresh.png
@@ -0,0 +1,6 @@
+‰PNG
+
+���
IHDR���@���@���ªiqÞ���	pHYs������rçn���tEXtSoftware�www.inkscape.org›î<��fIDATxœíÛO¨UUÇñÏõ)ùÊY”¤Yf`Ñrâ ÈI4h`“jPTHƒÄH‰ µ‚Ê(Ò0I¡À¢?Ð$5eÑÀHld†¾²’|¥ñÌ÷ž
ö½ê»ïþ9ûœ³¯÷Áý‚÷àìu×oÝskí³vEZúp=nÃÍX€y¸S0#8Ž¿ñ;àG|‡=8–8ÆÒ™ŠåxàtÁ><+$²«Yˆ8ª˜èVö-Ââ\„;qIc¸Û1,ðzÀjôGÄYÁË­ú8Š¥¹W™Šç0T²¸;„{«âÚ‰ßÒ`ý¾¼â—«ó%¼Þvcnño6Y7+¼‚•8Ù¢ëíOÜSï$lk³.3“±µ„¶²Q¬¾¨f·}®LÃg] 0«mÇ[¯mK?>ïQIlrñSð1îÈ’©Œü+<@«6Œé¸T¨g”øY…ÉòjgCøD(`åq+æá>¼+”ǩ<ZÐñ�žÀ¬6‚[яGðC§°H¸Uó8ü«„B©,ú°BñÞ"SúðMNg;qUyºÇq¹ðsJš€Çr:Ú$`¥Ð)–ž€™BOã`¤PÙ‚Šòê’1<¹xTxHu’
+6GÆ™)ÓÄ÷òëR©lBÙâÇ$àñÈ…_èÜož4âÇ$àûˆEƒBÁÒ)R‰?“€["½Në8*x=2¾èlˆXpRó͇¬‰ˆ-wbnÿ·Óimȯ±åJÀg7
+³ØMIåŽg "¶X;<	Wj¿©Xc'ö— *†×úÞX¾Fx{ÓŠcÂÃïP€š±wᢒüÀ|T’¿=zôèÑ£GŽZ)œ¢Ô܉KòÃÕxZØàmƈðõS¨Iªnku™Ê2²#cl‡kRöÛ¿%Úˆdoí÷65ç$h$¡ïF<){k¿»öGÊÝ–5Å0WÜèΙö?•ø­²eðRDl΍-…øÍ:+~¾°UŸ5¾µç.žèâûðeD|§Ô½Ó˜È≟ù^½ƒ‰,~EdŒ£¸µÞIâ¿ÒyñŠŸO~¿‘£²î€MÚ@•Á$¼˜#¾!¡LG™Ï€¯…éñTÌ—0bíxw2pZ8ý±–£aàj•0€•'¦½Â¨oCÊN@ÍŽà\V@øÅBCU¤_Ôæ´IªÔìv	ÃW‹µª˜ŒñpuÍ?{w·_©^ØI†pPøVÎYU[¨ÜùÂÕx¥ÝE©ï€óeë³f)‹³-ø D•.>K¶9{�a}ˆkeÃBuE+‡o_áÝ/®óê”
`Y¬xš—“Ä×X ÿ<q
+Û%Ìçb_¤ø}ÂH}ìhm™vD8[Pˆ¥Â‰«ÓB·´A\c3K˜ù«AYmP8N[ä,ÂfÇI¯+àcºp@"åyŸðT5Þ®f	^UÎIƒB§¹L‚ÑÜNôðsp»0^w­PíÍGÍÊÕBU8€_ð³ÐÀì­þŸŒÿ©žÉ¾æ¯����IEND®B`‚
\ No newline at end of file
diff --git a/resources/icons/ui/refresh.svg b/resources/icons/ui/refresh.svg
new file mode 100644
index 0000000000000000000000000000000000000000..17c41496b761b090e2df9e1c8d5e4aa5bed042e6
--- /dev/null
+++ b/resources/icons/ui/refresh.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   viewBox="-10 0 1792 1792"
+   id="svg866"
+   width="1792"
+   height="1792"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs870" />
+  <path
+     fill="currentColor"
+     d="m 1629,1056 q 0,5 -1,7 -64,268 -268,434.5 Q 1156,1664 882,1664 736,1664 599.5,1609 463,1554 356,1452 l -129,129 q -19,19 -45,19 -26,0 -45,-19 -19,-19 -19,-45 v -448 q 0,-26 19,-45 19,-19 45,-19 h 448 q 26,0 45,19 19,19 19,45 0,26 -19,45 l -137,137 q 71,66 161,102 90,36 187,36 134,0 250,-65 116,-65 186,-179 11,-17 53,-117 8,-23 30,-23 h 192 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 25,-800 v 448 q 0,26 -19,45 -19,19 -45,19 h -448 q -26,0 -45,-19 -19,-19 -19,-45 0,-26 19,-45 L 1235,521 Q 1087,384 886,384 q -134,0 -250,65 -116,65 -186,179 -11,17 -53,117 -8,23 -30,23 H 168 q -13,0 -22.5,-9.5 Q 136,749 136,736 v -7 Q 201,461 406,294.5 611,128 886,128 q 146,0 284,55.5 138,55.5 245,156.5 l 130,-129 q 19,-19 45,-19 26,0 45,19 19,19 19,45 z"
+     id="path864" />
+</svg>
diff --git a/resources/langs/nheko_cs.ts b/resources/langs/nheko_cs.ts
index 99813f81a8c1381bca0718e64149ef2964fc018c..9d5a913cc04693e259d0288533827b530560ee59 100644
--- a/resources/langs/nheko_cs.ts
+++ b/resources/langs/nheko_cs.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation type="unfinished"></translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation type="unfinished"></translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -182,7 +182,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -227,12 +227,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation type="unfinished"></translation>
     </message>
@@ -247,33 +247,35 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation type="unfinished"></translation>
     </message>
@@ -283,7 +285,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -293,7 +295,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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 filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished"></translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -756,25 +881,25 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -784,35 +909,53 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -862,18 +1005,23 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -901,7 +1049,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -924,7 +1072,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1073,32 +1233,28 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,27 +1391,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1283,33 +1445,41 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>RoomInfo</name>
     <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1415,16 +1608,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1454,7 +1667,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1469,7 +1687,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1515,12 +1743,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1540,8 +1768,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1549,21 +1777,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1618,6 +1874,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1670,18 +2041,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1701,7 +2072,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation type="unfinished">
@@ -1711,7 +2082,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1721,7 +2092,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1741,12 +2122,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1756,12 +2137,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1771,12 +2152,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1806,32 +2192,32 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1850,7 +2236,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1858,12 +2244,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1888,28 +2279,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1947,10 +2350,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1960,33 +2388,98 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+29"/>
+        <source>Room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2009,8 +2502,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2018,7 +2511,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2028,22 +2521,22 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2068,7 +2561,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2083,6 +2576,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2111,7 +2614,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2166,7 +2669,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2177,7 +2680,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2189,6 +2692,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2229,12 +2737,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2244,7 +2787,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2319,7 +2862,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2339,17 +2882,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2364,12 +2912,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2389,7 +2932,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2419,14 +2962,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2434,19 +2977,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2461,6 +3004,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2586,37 +3137,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2668,32 +3188,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2707,47 +3201,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2762,7 +3256,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2782,27 +3276,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2810,7 +3304,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts
index cb5b54fb6b751c1a9fa7cfa316fc62c61dbd4a45..538f67e2b292b8c8965c5b89f2f4569e5b8ef4a3 100644
--- a/resources/langs/nheko_de.ts
+++ b/resources/langs/nheko_de.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Wählt...</translation>
     </message>
@@ -17,17 +17,17 @@
     <message>
         <location line="+67"/>
         <source>You are screen sharing</source>
-        <translation>Du teilst deinen Bildschirm</translation>
+        <translation>Bildschirm wird geteilt</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Hide/Show Picture-in-Picture</source>
-        <translation>Bild-in-Bild Teilen/Verstecken</translation>
+        <translation>Bild-in-Bild zeigen/verstecken</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Unmute Mic</source>
-        <translation>Stummschaltung Aufheben</translation>
+        <translation>Mikrofon aktivieren</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Videoanruf</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Videoanruf</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Ganzer Bildschirm</translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Nutzer konnte nicht eingeladen werden: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Eingeladener Benutzer: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Das Migrieren des Caches auf die aktuelle Version ist fehlgeschlagen. Das kann verschiedene Gründe als Ursache haben. Bitte melde den Fehler und verwende in der Zwischenzeit eine ältere Version. Alternativ kannst du den Cache manuell löschen.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Beitritt bestätigen</translation>
     </message>
@@ -151,23 +151,23 @@
         <translation>Möchtest du wirklich %1 beitreten?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Raum %1 erzeugt.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Einladung bestätigen</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Nutzer %1 (%2) wirklich einladen?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Einladung von %1 in Raum %2 fehlgeschlagen: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Nutzer %1 (%2) wirklich kicken?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Gekickter Benutzer: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation>Nutzer %1 (%2) wirklich bannen?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>%1 konnte nicht aus %2 verbannt werden: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation>Bann des Nutzers %1 (%2) wirklich aufheben?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Verbannung von %1 aus %2 konnte nicht aufgehoben werden: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Verbannung aufgehoben: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation>Möchtest du wirklich eine private Konversation mit %1 beginnen?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Migration des Caches fehlgeschlagen!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>Der Cache auf der Festplatte wurde mit einer neueren Nheko - Version angelegt. Bitte aktualisiere Nheko oder entferne den Cache.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <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="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Bitte melde dich erneut an: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Konnte Raum nicht betreten: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Du hast den Raum betreten</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Einladung konnte nicht zurückgezogen werden: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Raum konnte nicht erstellt werden: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Konnte den Raum nicht verlassen: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation>Kontte %1 nicht aus %2 entfernen: %3</translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Geheimnisse entschlüsseln</translation>
     </message>
@@ -362,12 +364,12 @@
         <translation>Gib deinen Wiederherstellungsschlüssel oder dein Wiederherstellungspasswort ein um deine Geheimnisse zu entschlüsseln:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation>Gib deinen Wiederherstellungsschlüssel oder dein Wiederherstellungspasswort mit dem Namen %1 ein um deine Geheimnisse zu entschlüsseln:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Entschlüsseln fehlgeschlagen</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Suche</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Leute</translation>
     </message>
@@ -495,71 +497,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Diese Nachricht ist unverschlüsselt!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Kein Schlüssel für diese Nachricht vorhanden. Wir haben den Schlüssel automatisch angefragt, aber wenn du ungeduldig bist, kannst du den Schlüssel nocheinmal anfragen.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation>Verschlüsselt von einem verifizierten Gerät</translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Diese Nachricht konnte nicht entschlüsselt werden, weil unser Schlüssel nur für neuere Nachrichten gültig ist. Du kannst den Schlüssel für ältere Nachrichten anfragen.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation>Von einem unverifizierten Gerät verschlüsselt, Sie haben dem Nutzer jedoch früher schon vertraut.</translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Es ist ein interner Fehler beim Laden des Schlüssels aus der Datenbank aufgetreten.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation>Von einem unverifizierten Gerät verschlüsselt</translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Beim Entschlüsseln der Nachricht ist ein Fehler aufgetreten.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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üsseltes Event (keine Schlüssel zur Entschlüsselung gefunden) --</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Nheko hat die Nachricht nach der Entschlüsselung nicht verstanden.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation>-- Verschlüsseltes Event (Schlüssel passt nicht für diesen Nachrichtenindex) --</translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>Der Schlüssel für diese Nachricht wurde schon einmal verwendet! Vermutlich versucht jemand falsche Nachrichten in diese Unterhaltung einzufügen!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>-- Entschlüsselungsfehler (Fehler bei Suche nach Megolm Schlüsseln in Datenbank) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Unbekannter Entschlüsselungsfehler</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Entschlüsselungsfehler (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Schlüssel anfragen</translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Diese Nachricht ist unverschlüsselt!</translation>
     </message>
     <message>
-        <location line="-52"/>
-        <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üsseltes Event (Unbekannter Eventtyp) --</translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Verschlüsselt von einem verifizierten Gerät</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Replay-angriff! Der Nachrichtenindex wurde wiederverwendet! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Von einem unverifizierten Gerät verschlüsselt, Sie haben dem Nutzer jedoch früher schon vertraut.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Nachricht von einem unverifizierten Gerät! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Nachricht verschlüsselt bei einem unverifizierten Gerät oder der Schlüssel ist aus einer nicht vertrauenswürdigen Quelle wie der Onlineschlüsselsicherung.</translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Verifizierung abgelaufen, die andere Seite antwortet nicht.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Die andere Seite hat die Verifizierung abgebrochen.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Schließen</translation>
     </message>
@@ -604,48 +613,138 @@
         <translation>Nachricht weiterleiten</translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Bilderpackung bearbeiten</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Bilder hinzufügen</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Sticker (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>Eindeutiger Name</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Paketname</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Attribution</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Als Emoji verwenden</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Als Sticker verwenden</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Abkürzung</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Beschreibung</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Vom Paket entfernen</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Entfernen</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Abbrechen</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Spechern</translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Bilderpackungseinstellungen</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Neue Packung erstellen</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Neue Packung</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Private Packung</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Packung aus diesem Raum</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Global aktivierte Packung</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Global aktivieren</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Macht diese Packung in allen Räumen verfügbar</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Bearbeiten</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished">Schließen</translation>
+        <translation>Schließen</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Datei auswählen</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation>Alle Dateien (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Medienupload fehlgeschlagen. Bitte versuche es erneut.</translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation>Lade Benutzer in %1 ein</translation>
     </message>
@@ -694,6 +793,32 @@
         <translation>Abbrechen</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Raum-ID oder -Alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Raum verlassen</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Willst du wirklich den Raum verlassen?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -760,25 +885,25 @@ Beispiel: https://mein.server:8787</translation>
         <translation>ANMELDEN</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation>Du hast eine invalide Matrix ID eingegeben. Normalerwise sehen die so aus: @joe:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Automatische Erkennung fehlgeschlagen. Antwort war fehlerhaft.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Benötigte Ansprechpunkte nicht auffindbar. Möglicherweise kein Matrixserver.</translation>
     </message>
@@ -788,30 +913,48 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Erhaltene Antwort war fehlerhaft. Bitte Homeserverdomain prüfen.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Ein unbekannter Fehler ist aufgetreten. Bitte Homeserverdomain prüfen.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>SSO ANMELDUNG</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Leeres Passwort</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>SSO Anmeldung fehlgeschlagen</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>entfernt</translation>
@@ -822,7 +965,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Verschlüsselung aktiviert</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>Raumname wurde gändert auf: %1</translation>
     </message>
@@ -881,6 +1024,11 @@ Beispiel: https://mein.server:8787</translation>
         <source>Negotiating call...</source>
         <translation>Wählt…</translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Reinlassen</translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -905,7 +1053,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Schreibe eine Nachricht…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation>Sticker</translation>
     </message>
@@ -928,7 +1076,7 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Bearbeiten</translation>
     </message>
@@ -948,17 +1096,19 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Optionen</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation>&amp;Kopieren</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation>Kopiere &amp;Link</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation>Re&amp;agieren</translation>
     </message>
@@ -1017,6 +1167,11 @@ Beispiel: https://mein.server:8787</translation>
         <source>Copy link to eve&amp;nt</source>
         <translation>Link &amp;zu diesem Event kopieren</translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>&amp;Gehe zur zitierten Nachricht</translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Verifizierungsanfrage erhalten</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation>Damit andere Nutzer sehen, welche Geräte tatsächlich dir gehören, kannst du sie verifizieren. Das erlaubt auch Schlüsselbackup zu nutzen ohne ein Passwort einzugeben. %1 jetzt verifizieren?</translation>
     </message>
@@ -1076,33 +1236,29 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Akzeptieren</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 hat eine verschlüsselte Nachricht gesendet</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation>* %1 %2</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>%1 hat geantwortet: %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation>%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,7 +1289,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Kein Mikrofon gefunden.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Sprache</translation>
     </message>
@@ -1164,7 +1320,7 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation>Benutze ein separates profil, wodurch mehrere Accounts und Nhekoinstanzen zur gleichen Zeit verwendet werden können.</translation>
     </message>
@@ -1179,21 +1335,37 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Profilname</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Lesebestätigungen</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Gestern, %1</translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Benutzername</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Der Benutzername sollte nicht leer sein und nur aus a-z, 0-9, ., _, =, - und / bestehen.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Passwort</translation>
     </message>
@@ -1213,7 +1385,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Heimserver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Ein Server, der Registrierungen zulässt. Weil Matrix ein dezentralisiertes Protokoll ist, musst du erst einen Server ausfindig machen oder einen persönlichen Server aufsetzen.</translation>
     </message>
@@ -1223,27 +1395,17 @@ Beispiel: https://mein.server:8787</translation>
         <translation>REGISTRIEREN</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Keine unterstützten Registrierungsmethoden!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>Mindestens ein Feld hat invalide Werte. Bitte behebe diese Fehler und versuche es erneut.</translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Automatische Erkennung fehlgeschlagen. Antwort war fehlerhaft.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Benötigte Ansprechpunkte nicht auffindbar. Möglicherweise kein Matrixserver.</translation>
     </message>
@@ -1258,17 +1420,17 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Ein unbekannter Fehler ist aufgetreten. Bitte Homeserverdomain prüfen.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Passwort nicht lang genug (mind. 8 Zeichen)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Passwörter stimmen nicht überein</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Ungültiger Servername</translation>
     </message>
@@ -1276,7 +1438,7 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Schließen</translation>
     </message>
@@ -1286,10 +1448,28 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Bearbeiten abbrechen</translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Öffentliche Räume erkunden</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Suche nach öffentlichen Räumen</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation>keine Version gespeichert</translation>
     </message>
@@ -1297,7 +1477,7 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation>Neuer Tag</translation>
     </message>
@@ -1306,16 +1486,6 @@ Beispiel: https://mein.server:8787</translation>
         <source>Enter the tag you want to use:</source>
         <translation>Gib den Tag, den du verwenden willst, ein:</translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1347,7 +1517,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Neuen Tag erstellen...</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation>Statusnachricht</translation>
     </message>
@@ -1367,14 +1537,37 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Setze eine Statusnachricht</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation>Abmelden</translation>
     </message>
     <message>
-        <location line="+46"/>
-        <source>Start a new chat</source>
-        <translation>Neues Gespräch beginnen</translation>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Schließen</translation>
+    </message>
+    <message>
+        <location line="+65"/>
+        <source>Start a new chat</source>
+        <translation>Neues Gespräch beginnen</translation>
     </message>
     <message>
         <location line="+8"/>
@@ -1392,7 +1585,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Raumverzeichnis</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation>Benutzereinstellungen</translation>
     </message>
@@ -1400,12 +1593,12 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation>Teilnehmer in %1</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation>
@@ -1418,16 +1611,36 @@ Beispiel: https://mein.server:8787</translation>
         <source>Invite more people</source>
         <translation>Lade mehr Leute ein</translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Dieser Raum ist nicht verschlüsselt!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Der Nutzer ist verifiziert.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Der Nutzer ist nicht verifiziert, aber hat schon immer diese Identität verwendet.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Dieser Nutzer hat unverifizierte Geräte!</translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation>Raumeinstellungen</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation>%1 Teilnehmer</translation>
     </message>
@@ -1457,7 +1670,12 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Alle Nachrichten</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Zugangsberechtigungen</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation>Jeder (inkl. Gäste)</translation>
     </message>
@@ -1472,7 +1690,17 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Eingeladene Nutzer</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>Durch Anklopfen</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Durch Teilnahme an anderen Räumen</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Verschlüsselung</translation>
     </message>
@@ -1490,17 +1718,17 @@ Beispiel: https://mein.server:8787</translation>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Sticker- &amp; Emoteeinstellungen</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Ändern</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Ändere welche Packungen aktiviert sind, entferne oder erstelle neue Packungen</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -1518,12 +1746,12 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Raumversion</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation>Aktivierung der Verschlüsselung fehlgeschlagen: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Wähle einen Avatar</translation>
     </message>
@@ -1543,8 +1771,8 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Fehler beim Lesen der Datei: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation>Hochladen des Bildes fehlgeschlagen: %s</translation>
     </message>
@@ -1552,21 +1780,49 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation>Offene Einladung.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation>Vorschau dieses Raums</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation>Keine Vorschau verfügbar</translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1621,6 +1877,121 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Abbrechen</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Verbindung zum kryptografischen Speicher fehlgeschlagen</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Konnte die Bilderpackung nicht aktualisieren: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Konnte die alte Bilderpackung nicht löschen: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Konnte Bild nicht öffnen: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Konnte Bild nicht hochladen: %1</translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1673,18 +2044,18 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Nachricht zurückziehen fehlgeschlagen: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation>Event konnte nicht verschlüsselt werden, senden wurde abgebrochen!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Bild speichern</translation>
     </message>
@@ -1704,7 +2075,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Datei speichern</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1713,7 +2084,7 @@ Beispiel: https://mein.server:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 hat diesen Raum öffentlich gemacht.</translation>
     </message>
@@ -1723,7 +2094,17 @@ Beispiel: https://mein.server:8787</translation>
         <translation>%1 hat eingestellt, dass dieser Raum eine Einladung benötigt um beizutreten.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 hat erlaubt Leuten diesen Raum durch Anklopfen beizutreten.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 hat erlaubt Mitglieder aus folgenden Räumen diesen Raum automatisch zu betreten: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 hat Gästen erlaubt den Raum zu betreten.</translation>
     </message>
@@ -1743,12 +2124,12 @@ Beispiel: https://mein.server:8787</translation>
         <translation>%1 hat eingestellt, dass nur Teilnehmer Nachrichten in diesem Raum lesen können (ab diesem Punkt).</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 hat eingestellt, dass Teilnehmer die Historie dieses Raums lesen können ab dem Zeitpunkt, zu dem sie eingeladen wurden.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 hat eingestellt, dass Teilnehmer die Historie dieses Raums lesen können ab dem Zeitpunkt, zu dem sie beigetreten sind.</translation>
     </message>
@@ -1758,12 +2139,12 @@ Beispiel: https://mein.server:8787</translation>
         <translation>%1 hat die Berechtigungen dieses Raums bearbeitet.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 wurde eingeladen.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 hat den Avatar geändert.</translation>
     </message>
@@ -1773,12 +2154,17 @@ Beispiel: https://mein.server:8787</translation>
         <translation>%1 hat etwas im Profil geändert.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 hat den Raum betreten.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 hat den Raum durch Authorisierung von %2s Server betreten.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 hat die Einladung abgewiesen.</translation>
     </message>
@@ -1808,32 +2194,32 @@ Beispiel: https://mein.server:8787</translation>
         <translation>%1 wurde gebannt.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation>Grund: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 hat das Anklopfen zurückgezogen.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Du bist dem Raum beigetreten.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation>%1 hat den eigenen Avatar und Namen geändert zu %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation>%1 hat den eigenen Namen geändert zu %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Hat das Anklopfen von %1 abgewiesen.</translation>
     </message>
@@ -1852,7 +2238,7 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Bearbeitet</translation>
     </message>
@@ -1860,12 +2246,17 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Kein Raum geöffnet</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Keine Vorschau verfügbar</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation>%1 Teilnehmer</translation>
     </message>
@@ -1890,28 +2281,40 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Zurück zur Raumliste</translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation>Keinen verschlüsselten Chat mit diesem User gefunden. Erstelle einen verschlüsselten 1:1 Chat mit diesem Nutzer und versuche es erneut.</translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation>Zurück zur Raumliste</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation>Kein Raum ausgewählt</translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Dieser Raum ist nicht verschlüsselt!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Dieser Raum enthält nur verifizierte Geräte.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Dieser Raum enthält unverifizierte Geräte!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation>Raumoptionen</translation>
     </message>
@@ -1949,10 +2352,35 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Schließen</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Bitte gebe ein gültiges Registrierungstoken ein.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation>Globales Nutzerprofil</translation>
     </message>
@@ -1962,33 +2390,98 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Raumspezifisches Nutzerprofil</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Ändere das Profilbild in allen Räumen.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Ändere das Profilbild nur in diesem Raum.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Ändere den Anzeigenamen in allen Räumen.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Ändere den Anzeigenamen nur in diesem Raum.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Raum: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>Dies ist das raumspezifische Nutzerprofil. Der Anzeigename und das Profilbild kann sich von dem globalen Profil unterscheiden.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Öffne das globale Profil des Nutzers.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
         <translation>Verifizieren</translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
-        <translation>Banne den Nutzer</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Starte eine private Unterhaltung.</translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation>Starte eine private Konservation</translation>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Benutzer aus dem Raum werfen.</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
-        <translation>Kicke den Nutzer</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Benutzer aus dem Raum verbannen.</translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation>Verifizierung zurückziehen</translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Avatar wählen</translation>
     </message>
@@ -2011,8 +2504,8 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation>Standard</translation>
     </message>
@@ -2020,7 +2513,7 @@ Beispiel: https://mein.server:8787</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Ins Benachrichtigungsfeld minimieren</translation>
     </message>
@@ -2030,22 +2523,22 @@ Beispiel: https://mein.server:8787</translation>
         <translation>Im Benachrichtigungsfeld starten</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Gruppen-Seitenleiste</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Runde Profilbilder</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation>Profil: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation>Standard</translation>
     </message>
@@ -2070,7 +2563,7 @@ Beispiel: https://mein.server:8787</translation>
         <translation>DOWNLOADEN</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation>Applikation im Hintergrund weiterlaufen lassen.</translation>
     </message>
@@ -2086,6 +2579,16 @@ OFF - square, ON - Circle.</source>
         <translation>Ändert das Aussehen von Benutzeravataren.
 AUS - Quadratisch, AN - Kreis.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2115,7 +2618,7 @@ be blurred.</source>
         <translation>Die Zeitliste wird unscharf, wenn das Fenster den Fokus verliert.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation>Sichtschutz-Zeitbegrenzung (in Sekunden [0 - 3600])</translation>
     </message>
@@ -2175,7 +2678,7 @@ Wenn das aus ist, werden die Räume in der Raumliste rein nach dem Sendezeitpunk
 Wenn das eingeschaltet ist, werden Nachrichten mit aktiven Erwähnung zuerst sortiert (der rote Kreis). Danach kommen andere Benachrichtigungen (weißer Kreis) und zuletzt stummgeschaltete Räume sortiert nach deren Zeitstempel.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Lesebestätigungen</translation>
     </message>
@@ -2187,7 +2690,7 @@ Status is displayed next to timestamps.</source>
 Der Status wird neben der Nachricht angezeigt.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Sende Nachrichten als Markdown formatiert</translation>
     </message>
@@ -2200,6 +2703,11 @@ Wenn deaktiviert werden alle Nachrichten als unformatierter Text gesendet.</tran
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Animiete Bilder nur abspielen, wenn die Maus über diesen ist</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Desktopbenachrichtigungen</translation>
     </message>
@@ -2241,12 +2749,47 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Erhöht die Schriftgröße, wenn die Nachricht nur aus ein paar Emoji besteht.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Sende verschlüsselte Nachrichten nur an verifizierte Nutzer</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Sendet Schlüssel für verschlüsselte Nachrichten nur an verifizierte Geräte. Das erhöht die Sicherheit, aber macht die Ende-zu-Ende Verschlüsselung komplizierter, weil jeder Nutzer verifiziert werden muss.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation>Teile Schlüssel mit verifizierten Nutzern und Geräten</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Automatisch Schlüssel an verifizierte Nutzer weiterleiten, auch wenn der Nutzer eigentlich keinen Zugriff auf diese Schlüssel haben sollte.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Onlinenachrichtenschlüsselspeicher</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Speichere eine Kopie der Nachrichtenschlüssel verschlüsselt auf dem Server.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Onlinenachrichtenschlüsselspeicher aktivieren</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>Die Nhekoentwickler empfehlen aktuell nicht die Onlinesicherung zu ativieren bevor die symmetrische Methode zu verfügung steht. Trotzdem aktivieren?</translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation>IM CACHE</translation>
     </message>
@@ -2256,7 +2799,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>NICHT IM CACHE</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Skalierungsfaktor</translation>
     </message>
@@ -2331,7 +2874,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Gerätefingerabdruck</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Sitzungsschlüssel</translation>
     </message>
@@ -2351,17 +2894,22 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>VERSCHLÜSSELUNG</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>ALLGEMEINES</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>OBERFLÄCHE</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Spiele Medien wie GIF oder WEBP nur ab, wenn du die Maus darüber bewegst.</translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation>Touchscreenmodus</translation>
     </message>
@@ -2376,12 +2924,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Emojischriftart</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation>Antortet automatsch auf Schlüsselanfragen von anderen Nutzern, wenn du diese verifiziert hast.</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation>Masterverifizierungsschlüssel</translation>
     </message>
@@ -2401,7 +2944,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Der Schlüssel um andere Nutzer zu verifizieren. Wenn der lokal zwischengespeichert ist, dann werden durch eine Nutzerverifizierung alle Geräte verifiziert.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation>Selbstverifizierungsschlüssel</translation>
     </message>
@@ -2431,14 +2974,14 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Alle Dateien (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Öffne Sessions Datei</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2446,19 +2989,19 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Feher</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Password für Datei</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Bitte gib das Passwort zum Enschlüsseln der Datei ein:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>Das Passwort darf nicht leer sein</translation>
     </message>
@@ -2473,6 +3016,14 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Datei zum Speichern der zu exportierenden Sitzungsschlüssel</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Keinen verschlüsselten Chat mit diesem User gefunden. Erstelle einen verschlüsselten 1:1 Chat mit diesem Nutzer und versuche es erneut.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2598,37 +3149,6 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.</
         <translation>Öffne das Fallback, folge den Anweisungen und bestätige nach Abschluss via &quot;Bestätigen&quot;.</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Betreten</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Abbrechen</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Raum-ID oder -Alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Abbrechen</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Willst du wirklich den Raum verlassen?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2682,32 +3202,6 @@ Medien-Größe: %2
         <translation>Löse das reCAPTCHA und drücke den &quot;Bestätigen&quot;-Knopf</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Lesebestätigungen</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Schließen</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Heute %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Gestern %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2721,47 +3215,47 @@ Medien-Größe: %2
         <translation>%1 hat eine Audiodatei gesendet</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Du hast ein Bild gesendet</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 hat ein Bild gesendet</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Du hast eine Datei gesendet</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 hat eine Datei gesendet</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Du hast ein Video gesendet</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 hat ein Video gesendet</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Du hast einen Sticker gesendet</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 hat einen Sticker gesendet</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Du hast eine Benachrichtigung gesendet</translation>
     </message>
@@ -2776,7 +3270,7 @@ Medien-Größe: %2
         <translation>Du: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2796,27 +3290,27 @@ Medien-Größe: %2
         <translation>Du hast angerufen</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 hat angerufen</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Du hast einen Anruf angenommen</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 hat einen Anruf angenommen</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Du hast einen Anruf beendet</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 hat einen Anruf beendet</translation>
     </message>
@@ -2824,7 +3318,7 @@ Medien-Größe: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <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 d40a643316a59664e53f81e9d59b414d2df76c98..88503e8649f91c8c5353d92d897ff847dd9d16c0 100644
--- a/resources/langs/nheko_el.ts
+++ b/resources/langs/nheko_el.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation type="unfinished"></translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation type="unfinished"></translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -182,7 +182,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -227,12 +227,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation type="unfinished"></translation>
     </message>
@@ -247,33 +247,35 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation type="unfinished"></translation>
     </message>
@@ -283,7 +285,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -293,7 +295,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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 filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">Άκυρο</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished">Διάλεξε ένα αρχείο</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished">Όλα τα αρχεία (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">Άκυρο</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">ID ή όνομα συνομιλίας</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Βγές</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Είστε σίγουροι οτι θέλετε να κλείσετε τη συνομιλία;</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -756,25 +881,25 @@ Example: https://server.my:8787</source>
         <translation>ΕΙΣΟΔΟΣ</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -784,30 +909,48 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Κενός κωδικός</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
@@ -818,7 +961,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -877,6 +1020,11 @@ Example: https://server.my:8787</source>
         <source>Negotiating call...</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -901,7 +1049,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Γράψε ένα μήνυμα...</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -924,7 +1072,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1073,32 +1233,28 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Όνομα χρήστη</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Κωδικός</translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,27 +1391,17 @@ Example: https://server.my:8787</source>
         <translation>ΕΓΓΡΑΦΗ</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Ο κωδικός δεν αποτελείται από αρκετους χαρακτήρες</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Οι κωδικοί δεν ταιριίαζουν</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Λανθασμένο όνομα διακομιστή</translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1282,10 +1444,28 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1293,7 +1473,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1302,16 +1482,6 @@ Example: https://server.my:8787</source>
         <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1414,16 +1607,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1453,7 +1666,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1468,7 +1686,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1514,12 +1742,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1539,8 +1767,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1548,21 +1776,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1617,6 +1873,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Άκυρο</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1669,18 +2040,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation type="unfinished">Αποθήκευση Εικόνας</translation>
     </message>
@@ -1700,7 +2071,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation type="unfinished">
@@ -1709,7 +2080,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1719,7 +2090,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1739,12 +2120,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1754,12 +2135,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1769,12 +2150,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1804,32 +2190,32 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1848,7 +2234,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1856,12 +2242,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1886,28 +2277,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1945,10 +2348,35 @@ Example: https://server.my:8787</source>
         <translation>Έξοδος</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1958,33 +2386,98 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2007,8 +2500,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2016,7 +2509,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Ελαχιστοποίηση</translation>
     </message>
@@ -2026,22 +2519,22 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2066,7 +2559,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2081,6 +2574,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2109,7 +2612,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2175,7 +2678,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>ΓΕΝΙΚΑ</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished">Όλα τα αρχεία (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Άκυρο</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>ID ή όνομα συνομιλίας</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Άκυρο</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Είστε σίγουροι οτι θέλετε να κλείσετε τη συνομιλία;</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2666,32 +3186,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3199,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2760,7 +3254,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2780,27 +3274,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2808,7 +3302,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <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 1851fff1498570ee0bd0ee176e05bc3f755b429b..5768e05132dea0a6df2f5ce46304385e0342d530 100644
--- a/resources/langs/nheko_en.ts
+++ b/resources/langs/nheko_en.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Calling...</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Video Call</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Video Call</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Entire screen</translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Failed to invite user: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Invited user: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Confirm join</translation>
     </message>
@@ -151,23 +151,23 @@
         <translation>Do you really want to join %1?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Room %1 created.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Confirm invite</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Do you really want to invite %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Failed to invite %1 to %2: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Do you really want to kick %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Kicked user: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation>Do you really want to ban %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Failed to ban %1 in %2: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation>Do you really want to unban %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Failed to unban %1 in %2: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Unbanned user: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation>Do you really want to start a private chat with %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Cache migration failed!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Failed to restore OLM account. Please login again.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Failed to restore save data. Please login again.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <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="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Please try to login again: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Failed to join room: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>You joined the room</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Failed to remove invite: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Room creation failed: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Failed to leave room: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation>Failed to kick %1 from %2: %3</translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Decrypt secrets</translation>
     </message>
@@ -362,12 +364,12 @@
         <translation>Enter your recovery key or passphrase to decrypt your secrets:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation>Enter your recovery key or passphrase called %1 to decrypt your secrets:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Decryption failed</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Search</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>People</translation>
     </message>
@@ -495,71 +497,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>This message is not encrypted!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation>Encrypted by a verified device</translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation>Encrypted by an unverified device, but you have trusted that user so far.</translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>There was an internal error reading the decryption key from the database.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation>Encrypted by an unverified device</translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>There was an error decrypting this message.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>The message couldn&apos;t be parsed.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation>-- Encrypted Event (Key not valid for this index) --</translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Unknown decryption error</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Decryption Error (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Request key</translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>This message is not encrypted!</translation>
     </message>
     <message>
-        <location line="-52"/>
-        <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>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Encrypted by a verified device</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Replay attack! This message index was reused! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Encrypted by an unverified device, but you have trusted that user so far.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Message by unverified device! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Device verification timed out.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Other party canceled the verification.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation>Verification messages received out of order!</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation>Unknown verification error.</translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Close</translation>
     </message>
@@ -604,48 +613,138 @@
         <translation>Forward Message</translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Editing image pack</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Add images</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>State key</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Packname</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Attribution</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Use as Emoji</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Use as Sticker</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Shortcode</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Body</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Remove from pack</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Remove</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Cancel</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Save</translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Image pack settings</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Create account pack</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>New room pack</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Private pack</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Pack from this room</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Globally enabled pack</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Enable globally</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Enables this pack to be used in all rooms</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Edit</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished">Close</translation>
+        <translation>Close</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Select a file</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation>All Files (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Failed to upload media. Please try again.</translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation>Invite users to %1</translation>
     </message>
@@ -694,6 +793,32 @@
         <translation>Cancel</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation>Join room</translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation>Room ID or alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation>Leave room</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation>Are you sure you want to leave?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -760,25 +885,25 @@ Example: https://server.my:8787</translation>
         <translation>LOGIN</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation>You have entered an invalid Matrix ID  e.g @joe:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Autodiscovery failed. Received malformed response.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Autodiscovery failed. Unknown error while requesting .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>The required endpoints were not found. Possibly not a Matrix server.</translation>
     </message>
@@ -788,35 +913,53 @@ Example: https://server.my:8787</translation>
         <translation>Received malformed response. Make sure the homeserver domain is valid.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>An unknown error occured. Make sure the homeserver domain is valid.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>SSO LOGIN</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Empty password</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>SSO login failed</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation>Log out</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation>A call is in progress. Log out?</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation>Are you sure you want to log out?</translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation>Encryption enabled</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>room name changed to: %1</translation>
     </message>
@@ -866,18 +1009,23 @@ Example: https://server.my:8787</translation>
         <translation>Negotiating call…</translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Allow them in</translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation>%1 answered the call.</translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>removed</translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation>%1 ended the call.</translation>
     </message>
@@ -905,7 +1053,7 @@ Example: https://server.my:8787</translation>
         <translation>Write a message…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation>Stickers</translation>
     </message>
@@ -928,7 +1076,7 @@ Example: https://server.my:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Edit</translation>
     </message>
@@ -948,17 +1096,19 @@ Example: https://server.my:8787</translation>
         <translation>Options</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation>&amp;Copy</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation>Copy &amp;link location</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation>Re&amp;act</translation>
     </message>
@@ -1017,6 +1167,11 @@ Example: https://server.my:8787</translation>
         <source>Copy link to eve&amp;nt</source>
         <translation>Copy link to eve&amp;nt</translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>&amp;Go to quoted message</translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Example: https://server.my:8787</translation>
         <translation>Received Verification Request</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</translation>
     </message>
@@ -1076,33 +1236,29 @@ Example: https://server.my:8787</translation>
         <translation>Accept</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation>You are about to notify the whole room</translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 sent an encrypted message</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation>* %1 %2</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>%1 replied: %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation>%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,7 +1289,7 @@ Example: https://server.my:8787</translation>
         <translation>No microphone found.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Voice</translation>
     </message>
@@ -1164,7 +1320,7 @@ Example: https://server.my:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</translation>
     </message>
@@ -1179,21 +1335,37 @@ Example: https://server.my:8787</translation>
         <translation>profile name</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Read receipts</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Yesterday, %1</translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Username</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Password</translation>
     </message>
@@ -1213,7 +1385,7 @@ Example: https://server.my:8787</translation>
         <translation>Homeserver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</translation>
     </message>
@@ -1223,27 +1395,17 @@ Example: https://server.my:8787</translation>
         <translation>REGISTER</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>No supported registration flows!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>One or more fields have invalid inputs. Please correct those issues and try again.</translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Autodiscovery failed. Received malformed response.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Autodiscovery failed. Unknown error while requesting .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>The required endpoints were not found. Possibly not a Matrix server.</translation>
     </message>
@@ -1258,17 +1420,17 @@ Example: https://server.my:8787</translation>
         <translation>An unknown error occured. Make sure the homeserver domain is valid.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Password is not long enough (min 8 chars)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Passwords don&apos;t match</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Invalid server name</translation>
     </message>
@@ -1276,7 +1438,7 @@ Example: https://server.my:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Close</translation>
     </message>
@@ -1286,10 +1448,28 @@ Example: https://server.my:8787</translation>
         <translation>Cancel edit</translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Explore Public Rooms</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Search for public rooms</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation>Choose custom homeserver</translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation>no version stored</translation>
     </message>
@@ -1297,7 +1477,7 @@ Example: https://server.my:8787</translation>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation>New tag</translation>
     </message>
@@ -1306,16 +1486,6 @@ Example: https://server.my:8787</translation>
         <source>Enter the tag you want to use:</source>
         <translation>Enter the tag you want to use:</translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1347,7 +1517,7 @@ Example: https://server.my:8787</translation>
         <translation>Create new tag...</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation>Status Message</translation>
     </message>
@@ -1367,12 +1537,35 @@ Example: https://server.my:8787</translation>
         <translation>Set status message</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation>Logout</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation>Encryption not set up</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation>Unverified login</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation>Please verify your other devices</translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation>Close</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation>Start a new chat</translation>
     </message>
@@ -1392,7 +1585,7 @@ Example: https://server.my:8787</translation>
         <translation>Room directory</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation>User settings</translation>
     </message>
@@ -1400,12 +1593,12 @@ Example: https://server.my:8787</translation>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation>Members of %1</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation>
@@ -1418,16 +1611,36 @@ Example: https://server.my:8787</translation>
         <source>Invite more people</source>
         <translation>Invite more people</translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>This room is not encrypted!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>This user is verified.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>This user isn&apos;t verified, but is still using the same master key from the first time you met.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>This user has unverified devices!</translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation>Room Settings</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation>%1 member(s)</translation>
     </message>
@@ -1457,7 +1670,12 @@ Example: https://server.my:8787</translation>
         <translation>All messages</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Room access</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation>Anyone and guests</translation>
     </message>
@@ -1472,7 +1690,17 @@ Example: https://server.my:8787</translation>
         <translation>Invited users</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>By knocking</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Restricted by membership in other rooms</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Encryption</translation>
     </message>
@@ -1490,17 +1718,17 @@ Example: https://server.my:8787</translation>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Sticker &amp; Emote Settings</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Change</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Change what packs are enabled, remove packs or create new ones</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -1518,12 +1746,12 @@ Example: https://server.my:8787</translation>
         <translation>Room Version</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation>Failed to enable encryption: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Select an avatar</translation>
     </message>
@@ -1543,8 +1771,8 @@ Example: https://server.my:8787</translation>
         <translation>Error while reading file: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation>Failed to upload image: %s</translation>
     </message>
@@ -1552,21 +1780,49 @@ Example: https://server.my:8787</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation>Pending invite.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation>Previewing this room</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation>No preview available</translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation>Please enter your login password to continue:</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation>Please enter a valid email address to continue:</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation>Please enter a valid phone number to continue:</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation>Please enter the token, which has been sent to you:</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation>Wait for the confirmation link to arrive, then continue.</translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1621,6 +1877,123 @@ Example: https://server.my:8787</translation>
         <translation>Cancel</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Failed to connect to secret storage</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation>Encryption setup successfully</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation>Failed to setup encryption: %1</translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation>Setup Encryption</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation>Activate Encryption</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation>verify</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation>enter passphrase</translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation>Failed to create keys for cross-signing!</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation>Failed to create keys for online key backup!</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation>Failed to create keys secure server side secret storage!</translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation>Encryption Setup</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation>Encryption setup failed: %1</translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Failed to update image pack: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Failed to delete old image pack: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Failed to open image: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Failed to upload image: %1</translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1673,18 +2046,18 @@ Example: https://server.my:8787</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Message redaction failed: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation>Failed to encrypt event, sending aborted!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Save image</translation>
     </message>
@@ -1704,7 +2077,7 @@ Example: https://server.my:8787</translation>
         <translation>Save file</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1713,7 +2086,7 @@ Example: https://server.my:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 opened the room to the public.</translation>
     </message>
@@ -1723,7 +2096,17 @@ Example: https://server.my:8787</translation>
         <translation>%1 made this room require an invitation to join.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 allowed to join this room by knocking.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 allowed members of the following rooms to automatically join this room: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 made the room open to guests.</translation>
     </message>
@@ -1743,12 +2126,12 @@ Example: https://server.my:8787</translation>
         <translation>%1 set the room history visible to members from this point on.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 set the room history visible to members since they were invited.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 set the room history visible to members since they joined the room.</translation>
     </message>
@@ -1758,12 +2141,12 @@ Example: https://server.my:8787</translation>
         <translation>%1 has changed the room&apos;s permissions.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 was invited.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 changed their avatar.</translation>
     </message>
@@ -1773,12 +2156,17 @@ Example: https://server.my:8787</translation>
         <translation>%1 changed some profile info.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 joined.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 joined via authorisation from %2&apos;s server.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 rejected their invite.</translation>
     </message>
@@ -1808,32 +2196,32 @@ Example: https://server.my:8787</translation>
         <translation>%1 was banned.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation>Reason: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 redacted their knock.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>You joined this room.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation>%1 has changed their avatar and changed their display name to %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation>%1 has changed their display name to %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Rejected the knock from %1.</translation>
     </message>
@@ -1852,7 +2240,7 @@ Example: https://server.my:8787</translation>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Edited</translation>
     </message>
@@ -1860,12 +2248,17 @@ Example: https://server.my:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>No room open</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation>No preview available</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation>%1 member(s)</translation>
     </message>
@@ -1890,28 +2283,40 @@ Example: https://server.my:8787</translation>
         <translation>Back to room list</translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation>Back to room list</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation>No room selected</translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>This room is not encrypted!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>This room contains only verified devices.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation>This room contains verified devices and devices which have never changed their master key.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>This room contains unverified devices!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation>Room options</translation>
     </message>
@@ -1949,10 +2354,35 @@ Example: https://server.my:8787</translation>
         <translation>Quit</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation>No available registration flows!</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation>Registration aborted</translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation>Please enter a valid registration token.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation>Invalid token</translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation>Global User Profile</translation>
     </message>
@@ -1962,33 +2392,98 @@ Example: https://server.my:8787</translation>
         <translation>Room User Profile</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Change avatar globally.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Change avatar. Will only apply to this room.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Change display name globally.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Change display name. Will only apply to this room.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Room: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Open the global profile for this user.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
         <translation>Verify</translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
-        <translation>Ban the user</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Start a private chat.</translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation>Start a private chat</translation>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Kick the user.</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
-        <translation>Kick the user</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Ban the user.</translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation>Refresh device list.</translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation>Sign out this device.</translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation>Change device name.</translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation>Last seen %1 from %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation>Unverify</translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation>Sign out device %1</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation>You signed out this device.</translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Select an avatar</translation>
     </message>
@@ -2011,8 +2506,8 @@ Example: https://server.my:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation>Default</translation>
     </message>
@@ -2020,7 +2515,7 @@ Example: https://server.my:8787</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Minimize to tray</translation>
     </message>
@@ -2030,22 +2525,22 @@ Example: https://server.my:8787</translation>
         <translation>Start in tray</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Group&apos;s sidebar</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Circular Avatars</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation>profile: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation>Default</translation>
     </message>
@@ -2070,7 +2565,7 @@ Example: https://server.my:8787</translation>
         <translation>DOWNLOAD</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation>Keep the application running in the background after closing the client window.</translation>
     </message>
@@ -2086,6 +2581,16 @@ OFF - square, ON - Circle.</source>
         <translation>Change the appearance of user avatars in chats.
 OFF - square, ON - Circle.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation>Use identicons</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation>Display an identicon instead of a letter when a user has not set an avatar.</translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2116,7 +2621,7 @@ be blurred.</source>
 be blurred.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation>Privacy screen timeout (in seconds [0 - 3600])</translation>
     </message>
@@ -2176,7 +2681,7 @@ If this is off, the list of rooms will only be sorted by the timestamp of the la
 If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don&apos;t seem to consider them as important as the other rooms.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Read receipts</translation>
     </message>
@@ -2188,7 +2693,7 @@ Status is displayed next to timestamps.</source>
 Status is displayed next to timestamps.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Send messages as Markdown</translation>
     </message>
@@ -2201,6 +2706,11 @@ When disabled, all messages are sent as a plain text.</translation>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Play animated images only on hover</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Desktop notifications</translation>
     </message>
@@ -2242,12 +2752,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Make font size larger if messages with only a few emojis are displayed.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Send encrypted messages to verified users only</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation>Share keys with verified users and devices</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Online Key Backup</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Download message encryption keys from and upload to the encrypted online key backup.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Enable online key backup</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation>CACHED</translation>
     </message>
@@ -2257,7 +2802,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>NOT CACHED</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Scale factor</translation>
     </message>
@@ -2332,7 +2877,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Device Fingerprint</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Session Keys</translation>
     </message>
@@ -2352,17 +2897,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>ENCRYPTION</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>GENERAL</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>INTERFACE</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Plays media like GIFs or WEBPs only when explicitly hovering over them.</translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation>Touchscreen mode</translation>
     </message>
@@ -2377,12 +2927,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Emoji Font Family</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation>Automatically replies to key requests from other users, if they are verified.</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation>Master signing key</translation>
     </message>
@@ -2402,7 +2947,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>The key to verify other users. If it is cached, verifying a user will verify all their devices.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation>Self signing key</translation>
     </message>
@@ -2432,14 +2977,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>All Files (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Open Sessions File</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2447,19 +2992,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Error</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>File Password</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Enter the passphrase to decrypt the file:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>The password cannot be empty</translation>
     </message>
@@ -2474,6 +3019,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>File to save the exported session keys</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2599,37 +3152,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Open the fallback, follow the steps and confirm after completing them.</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Join</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Cancel</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Room ID or alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Cancel</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Are you sure you want to leave?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2683,32 +3205,6 @@ Media size: %2
         <translation>Solve the reCAPTCHA and press the confirm button</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Read receipts</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Close</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Today %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Yesterday %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2722,47 +3218,47 @@ Media size: %2
         <translation>%1 sent an audio clip</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>You sent an image</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 sent an image</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>You sent a file</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 sent a file</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>You sent a video</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 sent a video</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>You sent a sticker</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 sent a sticker</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>You sent a notification</translation>
     </message>
@@ -2777,7 +3273,7 @@ Media size: %2
         <translation>You: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2797,27 +3293,27 @@ Media size: %2
         <translation>You placed a call</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 placed a call</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>You answered a call</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 answered a call</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>You ended a call</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 ended a call</translation>
     </message>
@@ -2825,7 +3321,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Unknown Message Type</translation>
     </message>
diff --git a/resources/langs/nheko_eo.ts b/resources/langs/nheko_eo.ts
index a77c5c259a40719f03513ac095ed2b190a2b7f92..0d80dbcf3326da5a32dca9fdfd69839c4a4ea3c3 100644
--- a/resources/langs/nheko_eo.ts
+++ b/resources/langs/nheko_eo.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Vokante…</translation>
     </message>
@@ -22,7 +22,7 @@
     <message>
         <location line="+17"/>
         <source>Hide/Show Picture-in-Picture</source>
-        <translation type="unfinished">Kaŝi/Montri bildon en bildo</translation>
+        <translation>Kaŝi/Montri «bildon en bildo»</translation>
     </message>
     <message>
         <location line="+13"/>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Vidvoko</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Vidvoko</translation>
     </message>
@@ -117,31 +117,31 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
-        <translation type="unfinished">Tuta ekrano</translation>
+        <translation>Tuta ekrano</translation>
     </message>
 </context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Malsukcesis inviti uzanton: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
-        <translation type="unfinished">Invitita uzanto: %1</translation>
+        <translation>Invitita uzanto: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Malsukcesis migrado de kaŝmemoro al nuna versio. Tio povas havi diversajn kialojn. Bonvolu raporti eraron kaj dume provi malpli novan version. Alternative, vi povas provi forigi la kaŝmemoron permane.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Konfirmu aliĝon</translation>
     </message>
@@ -151,24 +151,24 @@
         <translation>Ĉu vi certe volas aliĝi al %1?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translatorcomment>I believe that the -at ending is correct here.</translatorcomment>
         <translation>Ĉambro %1 farit.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Konfirmu inviton</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Ĉu vi certe volas inviti uzanton %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Malsukcesis inviti uzanton %1 al %2: %3</translation>
     </message>
@@ -183,7 +183,7 @@
         <translation>Ĉu vi certe volas forpeli uzanton %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Forpelis uzanton: %1</translation>
     </message>
@@ -198,7 +198,7 @@
         <translation>Ĉu vi certe volas forbari uzanton %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Malsukcesis forbari uzanton %1 en %2: %3</translation>
     </message>
@@ -218,7 +218,7 @@
         <translation>Ĉu vi certe volas malforbari uzanton %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Malsukcesis malforbari uzanton %1 en %2: %3</translation>
     </message>
@@ -228,19 +228,19 @@
         <translation>Malforbaris uzanton: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation>Ĉu vi certe volas komenci privatan babilon kun %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Malsukcesis migrado de kaŝmemoro!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Incompatible cache version</source>
-        <translation type="unfinished">Neakorda versio de kaŝmemoro</translation>
+        <translation>Neakorda versio de kaŝmemoro</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -248,33 +248,35 @@
         <translation>La kaŝmemoro sur via disko estas pli nova ol kiom ĉi tiu versio de Nheko subtenas. Bonvolu ĝisdatigi la programon aŭ vakigi vian kaŝmemoron.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis rehavi konton je OLM. Bonvolu resaluti.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis rehavi konservitajn datumojn. Bonvolu resaluti.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Malsukcesis agordi ĉifrajn ŝlosilojn. Respondo de servilo: %1 %2. Bonvolu reprovi poste.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Bonvolu provi resaluti: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Malsukcesis aliĝi al ĉambro: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Vi aliĝis la ĉambron</translation>
     </message>
@@ -284,7 +286,7 @@
         <translation>Malsukcesis forigi inviton: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Malsukcesis krei ĉambron: %1</translation>
     </message>
@@ -294,7 +296,7 @@
         <translation>Malsukcesis eliri el ĉambro: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation>Malsukcesis forpeli uzanton %1 de %2: %3</translation>
     </message>
@@ -304,7 +306,7 @@
     <message>
         <location filename="../qml/CommunitiesList.qml" line="+44"/>
         <source>Hide rooms with this tag or from this space by default.</source>
-        <translation type="unfinished"></translation>
+        <translation>Implicite kaŝi ĉambrojn kun ĉi tiu etikedo aŭ de ĉi tiu aro.</translation>
     </message>
 </context>
 <context>
@@ -312,48 +314,48 @@
     <message>
         <location filename="../../src/timeline/CommunitiesModel.cpp" line="+37"/>
         <source>All rooms</source>
-        <translation type="unfinished">Ĉiuj ĉambroj</translation>
+        <translation>Ĉiuj ĉambroj</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Shows all rooms without filtering.</source>
-        <translation type="unfinished"></translation>
+        <translation>Montras ĉiujn ĉambrojn sen filtrado.</translation>
     </message>
     <message>
         <location line="+30"/>
         <source>Favourites</source>
-        <translation type="unfinished"></translation>
+        <translation>Elstaraj</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms you have favourited.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ĉambroj, kiujn vi elstarigis.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Low Priority</source>
-        <translation type="unfinished">Malalta prioritato</translation>
+        <translation>Malalta prioritato</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms with low priority.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ĉambroj kun malalta prioritato.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Server Notices</source>
-        <translation type="unfinished"></translation>
+        <translation>Avizoj de servilo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Messages from your server or administrator.</source>
-        <translation type="unfinished"></translation>
+        <translation>Mesaĝoj de via servilo aŭ administranto.</translation>
     </message>
 </context>
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Malĉifri sekretojn</translation>
     </message>
@@ -363,12 +365,12 @@
         <translation>Enigu vian rehavan ŝlosilon aŭ pasfrazon por malĉifri viajn sekretojn:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Enigu vian rehavan ŝlosilon aŭ pasfrazon kun nomo %1 por malĉifri viajn sekretojn:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Malsukcesis malĉifrado</translation>
     </message>
@@ -432,7 +434,7 @@
         <translation>Serĉu</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Homoj</translation>
     </message>
@@ -496,71 +498,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Ĉi tiu mesaĝo ne estas ĉifrita!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Estas neniu ŝloslio por malŝlosi ĉi tiun mesaĝon. Ni petis ĝin memage, sed vi povas provi repeti ĝin, se vi rapidas.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation>Ĉifrita de kontrolita aparato</translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Ne povis malĉifri ĉi tiun mesaĝon, ĉar ni havas nur ŝlosilon por pli novaj. Vi povas provi peti aliron al ĉi tiu mesaĝo.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation>Ĉifrita de nekontrolita aparato, sed vi fidis je tiu uzanto ĝis nun.</translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Eraris interne legado de malĉifra ŝlosilo el la datumbazo.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation>Ĉifrita de nekontrolita aparato</translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Eraris malĉifrado de ĉi tiu mesaĝo.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Ne povis trakti la mesaĝon.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>La ĉifra ŝlosilo estas reuzita! Eble iu provas enmeti falsitajn mesaĝojn en la babilon!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Nekonata malĉifra eraro</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation type="unfinished"></translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Peti ŝlosilon</translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Ĉi tiu mesaĝo ne estas ĉifrita!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Ĉifrita de kontrolita aparato</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Ĉifrita de nekontrolita aparato, sed vi fidis je tiu uzanto ĝis nun.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Ĉifrita de nekontrolita aparato, aŭ per ŝlosilo de nefidata fonto, ekzemple la deponejo de ŝlosiloj.</translation>
     </message>
 </context>
 <context>
@@ -582,17 +582,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Trafiĝis tempolimo de aparata kontrolo.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Aliulo nuligis la kontrolon.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Fermi</translation>
     </message>
@@ -602,7 +611,82 @@
     <message>
         <location filename="../qml/ForwardCompleter.qml" line="+44"/>
         <source>Forward Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Plusendi mesaĝon</translation>
+    </message>
+</context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Redaktado de bildopako</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Aldoni bildojn</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Glumarkoj (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>Identigilo (stata ŝlosilo)</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Nomo de pako</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Atribuo</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Uzi kiel bildosignon</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Uzi kiel glumarkon</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Mallongigo</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Korpo</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Forigi de pako</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Forigi</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Nuligi</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Konservi</translation>
     </message>
 </context>
 <context>
@@ -610,45 +694,60 @@
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Agordoj de bildopako</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Krei kontan pakon</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Nova ĉambra pako</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Privata pako</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Pakoj el ĉi tiu ĉambro</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Ĉie ŝaltita pako</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Ŝalti ĉie</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Ŝaltas ĉi tiun pakon por uzo en ĉiuj ĉambroj</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Redakti</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished">Fermi</translation>
+        <translation>Fermi</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
-        <translation type="unfinished">Elektu dosieron</translation>
+        <translation>Elektu dosieron</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -656,7 +755,7 @@
         <translation>Ĉiuj dosieroj (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Malsukcesis alŝuti vidaŭdaĵojn. Bonvolu reprovi.</translation>
     </message>
@@ -664,35 +763,61 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Invitu uzantojn al %1</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>User ID to invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Identigilo de invitota uzanto</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@tacuo:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>Aldoni</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Inviti</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
+        <translation>Nuligi</translation>
+    </message>
+</context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Identigilo aŭ kromnomo de ĉambro</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Eliri el ĉambro</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Ĉu vi certas, ke vi volas foriri?</translation>
     </message>
 </context>
 <context>
@@ -713,7 +838,8 @@
 You can also put your homeserver address there, if your server doesn&apos;t support .well-known lookup.
 Example: @user:server.my
 If Nheko fails to discover your homeserver, it will show you a field to enter the server manually.</source>
-        <translation type="unfinished">Via saluta nomo. Identigilo de Matrikso devus komenciĝi per @ sekvata de la identigilo de uzanto. Post la identigilo, vi devas meti retnomon post :. Vi ankaŭ povas enmeti adreson de via hejmservilo, se via servilo ne subtenas bone-konatan trovmanieron.
+        <translation>Via saluta nomo. Matriksa identigilo devus komenciĝi per @ sekvata de la identigilo de uzanto. Post la identigilo, vi devas meti nomon de via servilo post :.
+Vi ankaÅ­ povas enmeti adreson de via hejmservilo, se via servilo ne subtenas bone-konatan trovmanieron.
 Ekzemplo: @uzanto:servilo.mia
 Se Nheko malsukcesas trovi vian hejmservilon, ĝi montros kampon por ĝia permana aldono.</translation>
     </message>
@@ -763,58 +889,76 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>SALUTI</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation>Vi enigis nevalidan identigilon de Matrikso  ekz. @tacuo:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis memaga trovado. Ricevis misformitan respondon.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis memaga trovado. Okazis nekonata eraro dum petado. .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished"></translation>
+        <translation>La bezonataj konektaj lokoj ne troviĝis. Eble tio ne estas Matriksa servilo.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ricevis misformitan respondon. Certiĝu, ke retnomo de la hejmservilo estas valida.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Okazis nekonata eraro. Certiĝu, ke retnomo de la hejmservilo estas valida.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>UNUNURA SALUTO</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Malplena pasvorto</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>Malsukcesis ununura saluto</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>forigita</translation>
@@ -822,52 +966,52 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+9"/>
         <source>Encryption enabled</source>
-        <translation type="unfinished"></translation>
+        <translation>Ĉifrado estas ŝaltita</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>Nomo da ĉambro ŝanĝiĝis al: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed room name</source>
-        <translation type="unfinished"></translation>
+        <translation>forigis nomon de ĉambro</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>topic changed to: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>temo ŝanĝiĝis al: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed topic</source>
-        <translation type="unfinished"></translation>
+        <translation>forigis temon</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 changed the room avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 ŝanĝis bildon de la ĉambro</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 created and configured room: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 kreis kaj agordis ĉambron: %2</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>%1 placed a voice call.</source>
-        <translation>%1 metis voĉvokon.</translation>
+        <translation>%1 voĉvokis.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 placed a video call.</source>
-        <translation>%1 metis vidvokon.</translation>
+        <translation>%1 vidvokis.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 placed a call.</source>
-        <translation>%1 metis vokon.</translation>
+        <translation>%1 vokis.</translation>
     </message>
     <message>
         <location line="+14"/>
@@ -884,13 +1028,18 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <source>Negotiating call...</source>
         <translation>Traktante vokon…</translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Enlasi ĝin</translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
     <message>
         <location filename="../qml/MessageInput.qml" line="+44"/>
         <source>Hang up</source>
-        <translation type="unfinished"></translation>
+        <translation>Fini</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -900,7 +1049,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+25"/>
         <source>Send a file</source>
-        <translation type="unfinished">Sendu dosieron</translation>
+        <translation>Sendi dosieron</translation>
     </message>
     <message>
         <location line="+50"/>
@@ -908,9 +1057,9 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Skribu mesaĝon…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>Glumarkoj</translation>
     </message>
     <message>
         <location line="+24"/>
@@ -931,7 +1080,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Redakti</translation>
     </message>
@@ -951,74 +1100,81 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Elektebloj</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Kopii</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
-        <translation type="unfinished"></translation>
+        <translation>Kopii celon de &amp;ligilo</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
-        <translation type="unfinished"></translation>
+        <translation>Re&amp;agi</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Repl&amp;y</source>
-        <translation type="unfinished"></translation>
+        <translation>Re&amp;spondi</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>R&amp;edakti</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Read receip&amp;ts</source>
-        <translation type="unfinished"></translation>
+        <translation>K&amp;vitancoj</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>&amp;Forward</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Plusendi</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>&amp;Mark as read</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Marki legita</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>View raw message</source>
-        <translation type="unfinished">Vidi krudan mesaĝon</translation>
+        <translation>Vidi krudan mesaĝon</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>View decrypted raw message</source>
-        <translation type="unfinished">Vidi malĉifritan krudan mesaĝon</translation>
+        <translation>Vidi malĉifritan krudan mesaĝon</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Remo&amp;ve message</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Forigi mesaĝon</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Save as</source>
-        <translation type="unfinished"></translation>
+        <translation>Kon&amp;servi kiel</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Open in external program</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Malfermi per aparta programo</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Copy link to eve&amp;nt</source>
-        <translation type="unfinished"></translation>
+        <translation>Kopii ligilon al oka&amp;zo</translation>
+    </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>&amp;Iri al citita mesaĝo</translation>
     </message>
 </context>
 <context>
@@ -1034,7 +1190,12 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Ricevita kontrolpeto</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation>Por vidigi al aliuloj, kiuj viaj aparatoj vere apartenas al vi, vi povas ilin kontroli. Tio ankaŭ funkciigus memagan savkopiadon de ŝlosiloj. Ĉu vi volas kontroli aparaton %1 nun?</translation>
     </message>
@@ -1079,33 +1240,29 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Akcepti</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 sendis ĉifritan mesaĝon</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation>* %1 %2</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>%1 respondis: %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation>%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,10 +1290,10 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+16"/>
         <source>No microphone found.</source>
-        <translation type="unfinished">Neniu mikrofono troviĝis.</translation>
+        <translation>Neniu mikrofono troviĝis.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Voĉe</translation>
     </message>
@@ -1167,9 +1324,9 @@ Ekzemplo: https://servilo.mia:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
-        <translation type="unfinished"></translation>
+        <translation>Krei unikan profilon, kiu permesos al vi saluti kelkajn kontojn samtempe, kaj startigi plurajn nhekojn.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -1182,21 +1339,37 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>nomo de profilo</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Kvitancoj</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>HieraÅ­, %1</translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Uzantonomo</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>La uzantonomo devas ne esti malplena, kaj devas enhavi nur la signojn a–z, 0–9, ., _, =, -, kaj /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Pasvorto</translation>
     </message>
@@ -1216,7 +1389,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Hejmservilo</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Servilo, kiu permesas registriĝon. Ĉar Matrikso estas federa, vi bezonas unue trovi servilon, kie vi povus registriĝi, aŭ gastigi vian propran.</translation>
     </message>
@@ -1226,52 +1399,42 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>REGISTRIÄœI</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Neniuj subtenataj manieroj de registriĝo!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>Unu aÅ­ pliaj kampoj havas nevalidajn enigojn. Bonvolu korekti la problemojn kaj reprovi.</translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis memaga trovado. Ricevis misformitan respondon.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis memaga trovado. Okazis nekonata eraro dum petado. .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished"></translation>
+        <translation>La bezonataj konektaj lokoj ne troviĝis. Eble tio ne estas Matriksa servilo.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ricevis misformitan respondon. Certiĝu, ke retnomo de la hejmservilo estas valida.</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Okazis nekonata eraro. Certiĝu, ke retnomo de la hejmservilo estas valida.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Pasvorto nesufiĉe longas (almenaŭ 8 signoj)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Pasvortoj ne akordas</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Nevalida nomo de servilo</translation>
     </message>
@@ -1279,7 +1442,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Fermi</translation>
     </message>
@@ -1289,148 +1452,199 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Nuligi redakton</translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Esplori publikajn ĉambrojn</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Serĉi publikajn ĉambrojn</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
-        <translation type="unfinished"></translation>
+        <translation>neniu versio konservita</translation>
     </message>
 </context>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
-        <translation type="unfinished"></translation>
+        <translation>Nova etikedo</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter the tag you want to use:</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
+        <translation>Enigu la etikedon, kiun vi volas uzi:</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
-        <translation type="unfinished">Eliri el ĉambro</translation>
+        <translation>Eliri el ĉambro</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Tag room as:</source>
-        <translation type="unfinished">Etikedi ĉambron:</translation>
+        <translation>Etikedi ĉambron:</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Favourite</source>
-        <translation type="unfinished">Preferata</translation>
+        <translation>Elstara</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Low priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Malalta prioritato</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Server notice</source>
-        <translation type="unfinished"></translation>
+        <translation>Avizo de servilo</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Create new tag...</source>
-        <translation type="unfinished"></translation>
+        <translation>Krei novan etikedon…</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Statmesaĝo</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter your status message:</source>
-        <translation type="unfinished"></translation>
+        <translation>Enigu vian statmesaĝon:</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Profile settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Agordoj de profilo</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Set status message</source>
-        <translation type="unfinished"></translation>
+        <translation>Meti statmesaĝon</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
-        <translation type="unfinished">AdiaÅ­i</translation>
+        <translation>AdiaÅ­i</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Fermi</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
-        <translation type="unfinished">Komenci novan babilon</translation>
+        <translation>Komenci novan babilon</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Join a room</source>
-        <translation type="unfinished">Aliĝi ĉambron</translation>
+        <translation>Aliĝi al ĉambro</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Create a new room</source>
-        <translation type="unfinished"></translation>
+        <translation>Krei novan ĉambron</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Room directory</source>
-        <translation type="unfinished">Ĉambra dosierujo</translation>
+        <translation>Katalogo de ĉambroj</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
-        <translation type="unfinished">Agordoj de uzanto</translation>
+        <translation>Agordoj de uzanto</translation>
     </message>
 </context>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Anoj de %1</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%n persono en %1</numerusform>
+            <numerusform>%n personoj en %1</numerusform>
         </translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Invite more people</source>
-        <translation type="unfinished"></translation>
+        <translation>Inviti pliajn personojn</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ĉi tiu ĉambro ne estas ĉifrata!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Ĉi tiu uzanto estas kontrolita.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Ĉi tiu uzanto ne estas kontrolita, sed ankoraŭ uzas la saman ĉefan ŝlosilon ekde kiam vi renkontiĝis.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Ĉi tiu uzanto havas nekontrolitajn aparatojn!</translation>
     </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation>Agordoj de ĉambro</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation>%1 ĉambrano(j)</translation>
     </message>
@@ -1460,7 +1674,12 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Ĉiuj mesaĝoj</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Aliro al ĉambro</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation>Ĉiu ajn, inkluzive gastojn</translation>
     </message>
@@ -1475,7 +1694,17 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Invititoj</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>Per frapado</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Limigita de aneco en aliaj ĉambroj</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Ĉifrado</translation>
     </message>
@@ -1493,17 +1722,17 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Agordoj de glumarkoj kaj mienetoj</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Ŝanĝi</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Åœalti, forigi, aÅ­ krei novajn pakojn</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -1521,19 +1750,19 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Versio de ĉambro</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation>Malsukcesis ŝalti ĉifradon: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Elektu bildon de ĉambro</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Ĉiuj dosieroj (*)</translation>
+        <translation>Ĉiuj dosieroj (*)</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -1546,8 +1775,8 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Eraris legado de dosiero: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation>Malsukcesis alŝuti bildon: %s</translation>
     </message>
@@ -1555,18 +1784,46 @@ Ekzemplo: https://servilo.mia:8787</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>Atendanta invito.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Antaŭrigardante ĉi tiun ĉambron</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
+        <translation>Neniu antaÅ­rigardo disponeblas</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1590,18 +1847,18 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+19"/>
         <source>Include your camera picture-in-picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Enigi vian filmilon en la filmon</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Request remote camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Peti foran filmilon</translation>
     </message>
     <message>
         <location line="+1"/>
         <location line="+9"/>
         <source>View your callee&apos;s camera like a regular video call</source>
-        <translation type="unfinished"></translation>
+        <translation>Vidi la filmilon de via vokato kiel en ordinara vidvoko</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -1611,12 +1868,12 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+20"/>
         <source>Share</source>
-        <translation type="unfinished"></translation>
+        <translation>Vidigi</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Preview</source>
-        <translation type="unfinished"></translation>
+        <translation>AntaÅ­rigardi</translation>
     </message>
     <message>
         <location line="+7"/>
@@ -1624,6 +1881,121 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Nuligi</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Malsukcesis konektiĝi al sekreta deponejo</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Malsukcesis ĝisdatigi bildopakon: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Malsukcesis forigi malnovan bildopakon: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Malsukcesis malfermi bildon: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Malsukcesis alŝuti bildon: %1</translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1653,7 +2025,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
         <source>Search</source>
-        <translation type="unfinished">Serĉu</translation>
+        <translation>Serĉu</translation>
     </message>
 </context>
 <context>
@@ -1661,34 +2033,34 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location filename="../qml/device-verification/Success.qml" line="+11"/>
         <source>Successful Verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Sukcesis kontrolo</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Verification successful! Both sides verified their devices!</source>
-        <translation type="unfinished"></translation>
+        <translation>Sukcesis kontrolo! AmbaÅ­ flankoj kontrolis siajn aparatojn!</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Close</source>
-        <translation type="unfinished">Fermi</translation>
+        <translation>Fermi</translation>
     </message>
 </context>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis redaktado de mesaĝo: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
-        <translation type="unfinished"></translation>
+        <translation>Malsukcesis ĉifri okazon; sendado nuliĝis!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Konservi bildon</translation>
     </message>
@@ -1708,7 +2080,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>Konservi dosieron</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1717,7 +2089,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 malfermis la ĉambron al publiko.</translation>
     </message>
@@ -1727,7 +2099,17 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>%1 ekpostulis inviton por aliĝoj al la ĉamrbo.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 permesis aliĝi al ĉi tiu ĉambro per frapado.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 permesis al anoj de la jenaj ĉambroj memage aliĝi al ĉi tiu ĉambro: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 malfermis la ĉambron al gastoj.</translation>
     </message>
@@ -1747,28 +2129,28 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>%1 videbligis historion de la ĉambro al ĉiuj ĉambranoj ekde nun.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 videbligis historion de la ĉambro al ĉambranoj ekde ties invito.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 videbligis historion de la ĉambro al ĉambranoj ekde ties aliĝo.</translation>
     </message>
     <message>
         <location line="+22"/>
         <source>%1 has changed the room&apos;s permissions.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 ŝanĝis permesojn de la ĉambro.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translatorcomment>%1 estis invitata.</translatorcomment>
         <translation>%1 estis invitita.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translatorcomment>%1 ŝanĝis sian avataron.</translatorcomment>
         <translation>%1 ŝanĝis sian profilbildon.</translation>
@@ -1776,22 +2158,27 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+2"/>
         <source>%1 changed some profile info.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 ŝanĝis iujn informojn en profilo.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 aliĝis.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 aliĝis per rajtigo de servilo de %2.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 rifuzis sian inviton.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Revoked the invite to %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nuligis la inviton por %1.</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -1801,47 +2188,47 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+2"/>
         <source>Kicked %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Forpelis uzanton %1.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Unbanned %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malforbaris uzanton %1.</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>%1 was banned.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 estas forbarita.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Kialo: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 forigis sian frapon.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Vi aliĝis ĉi tiun ĉambron.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 ŝanĝis sian profilbildon kaj sian prezentan nomon al %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 ŝanĝis sian prezentan nomon al %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Rifuzis la frapon de %1.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -1852,13 +2239,13 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+10"/>
         <source>%1 knocked.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 frapis.</translation>
     </message>
 </context>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Redaktita</translation>
     </message>
@@ -1866,70 +2253,87 @@ Ekzemplo: https://servilo.mia:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
-        <translation type="unfinished"></translation>
+        <translation>Neniu ĉambro estas malfermita</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Neniu antaÅ­rigardo disponeblas</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished">%1 ĉambrano(j)</translation>
+        <translation>%1 ĉambrano(j)</translation>
     </message>
     <message>
         <location line="+33"/>
         <source>join the conversation</source>
-        <translation type="unfinished"></translation>
+        <translation>aliĝi al interparolo</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>accept invite</source>
-        <translation type="unfinished"></translation>
+        <translation>akcepti inviton</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>decline invite</source>
-        <translation type="unfinished"></translation>
+        <translation>rifuzi inviton</translation>
     </message>
     <message>
         <location line="+27"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Reen al listo de ĉambroj</translation>
     </message>
 </context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
+        <translation>Reen al listo de ĉambroj</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
+        <translation>Neniu ĉambro estas elektita</translation>
+    </message>
+    <message>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ĉi tiu ĉambro ne estas ĉifrata!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Ĉi tiu ĉambro enhavas nur kontrolitajn aparatojn.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Ĉi tiu ĉambro enhavas nekontrolitajn aparatojn!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
-        <translation type="unfinished"></translation>
+        <translation>Elektebloj de ĉambro</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Invite users</source>
-        <translation type="unfinished"></translation>
+        <translation>Inviti uzantojn</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Members</source>
-        <translation type="unfinished">Membroj</translation>
+        <translation>Anoj</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -1945,63 +2349,153 @@ Ekzemplo: https://servilo.mia:8787</translation>
 <context>
     <name>TrayIcon</name>
     <message>
-        <location filename="../../src/TrayIcon.cpp" line="+112"/>
-        <source>Show</source>
-        <translation>Montri</translation>
+        <location filename="../../src/TrayIcon.cpp" line="+112"/>
+        <source>Show</source>
+        <translation>Montri</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Quit</source>
+        <translation>Ĉesigi</translation>
+    </message>
+</context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Bonvolu enigi validan registran pecon.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>UserProfile</name>
+    <message>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
+        <source>Global User Profile</source>
+        <translation>Ĉiea profilo de uzanto</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Room User Profile</source>
+        <translation>Ĉambra profilo de uzanto</translation>
+    </message>
+    <message>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Ŝanĝi bildon ĉie.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Ŝanĝi bildon. Efektiviĝos nur en ĉi tiu ĉambro.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Ŝanĝi prezentan nomon ĉie.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Ŝanĝi prezentan nomon. Efektiviĝos nur en ĉi tiu ĉambro.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Ĉambro: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>Ĉi tio estas profilo speciala por ĉambro. La nomo kaj profilbildo de la uzanto povas esti malsamaj de siaj ĉieaj versioj.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Malfermi la ĉiean profilon de ĉi tiu uzanto.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation>Kontroli</translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Komenci privatan babilon.</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Forpeli la uzanton.</translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Quit</source>
-        <translation type="unfinished"></translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Forbari la uzanton.</translation>
     </message>
-</context>
-<context>
-    <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
-        <source>Global User Profile</source>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+0"/>
-        <source>Room User Profile</source>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+31"/>
+        <source>Change device name.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation type="unfinished"></translation>
+        <location line="+27"/>
+        <source>Unverify</source>
+        <translation>Malkontroli</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
-        <source>Unverify</source>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Elektu profilbildon</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Ĉiuj dosieroj (*)</translation>
+        <translation>Ĉiuj dosieroj (*)</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -2017,43 +2511,43 @@ Ekzemplo: https://servilo.mia:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Implicita</translation>
     </message>
 </context>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
-        <translation type="unfinished"></translation>
+        <translation>Etigi al plato</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Start in tray</source>
-        <translation type="unfinished"></translation>
+        <translation>Komenci ete sur pleto</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
-        <translation type="unfinished"></translation>
+        <translation>Flanka breto de grupoj</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
-        <translation type="unfinished"></translation>
+        <translation>Rondaj profilbildoj</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>profilo: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Implicita</translation>
     </message>
     <message>
         <location line="+31"/>
@@ -2063,7 +2557,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
     <message>
         <location line="+46"/>
         <source>Cross Signing Keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Åœlosiloj por delegaj subskriboj</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2076,7 +2570,7 @@ Ekzemplo: https://servilo.mia:8787</translation>
         <translation>ELÅœUTI</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation>Daŭrigi la aplikaĵon fone post fermo de la klienta fenestro.</translation>
     </message>
@@ -2092,6 +2586,16 @@ OFF - square, ON - Circle.</source>
         <translation>Ŝanĝas la aspekton de profilbildoj de uzantoj en babilujo.
 NE – kvadrataj, JES – rondaj.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2122,7 +2626,7 @@ be blurred.</source>
 malklariĝos.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation>Atendo ĝis privateca ŝirmilo (0–3600 sekundoj)</translation>
     </message>
@@ -2134,7 +2638,7 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds
         <translation>Tempo de atendo (en sekundoj) ekde senfokusiĝo
 de la fenestro, post kiu la enhavo malklariĝos.
 Agordu al 0 por malklarigi enhavon tuj post senfokusiĝo.
-Maksimuma valoro estas 1 horo (3600 sekundoj).</translation>
+Maksimuma valoro estas 1 horo (3600 sekundoj)</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -2189,7 +2693,7 @@ kiujn vi silentigis, ankoraŭ estos ordigitaj laŭ tempo, ĉar vi
 probable ne pensas ilin same gravaj kiel la aliaj ĉambroj.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Kvitancoj</translation>
     </message>
@@ -2201,7 +2705,7 @@ Status is displayed next to timestamps.</source>
 Stato estas montrita apud tempindikoj.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Sendi mesaĝojn Markdaŭne</translation>
     </message>
@@ -2214,6 +2718,11 @@ Kun ĉi tio malŝaltita, ĉiuj mesaĝoj sendiĝas en plata teksto.</translation>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Ludi movbildojn nur sub musmontrilo</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Labortablaj sciigoj</translation>
     </message>
@@ -2225,218 +2734,254 @@ Kun ĉi tio malŝaltita, ĉiuj mesaĝoj sendiĝas en plata teksto.</translation>
     <message>
         <location line="+1"/>
         <source>Alert on notification</source>
-        <translation type="unfinished"></translation>
+        <translation>Atentigi pri sciigoj</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show an alert when a message is received.
 This usually causes the application icon in the task bar to animate in some fashion.</source>
-        <translation type="unfinished"></translation>
+        <translation>Atentigas je ricevo de mesaĝo.
+Ĉi tio kutime movbildigas la simbolbildon sur la pleto iumaniere.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Highlight message on hover</source>
-        <translation type="unfinished"></translation>
+        <translation>Emfazi mesaĝojn sub musmontrilo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the background color of messages when you hover over them.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ŝanĝi fonkoloron de mesaĝoj sub musmontrilo.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Large Emoji in timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Grandaj bildosignoj en historio</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Make font size larger if messages with only a few emojis are displayed.</source>
-        <translation type="unfinished"></translation>
+        <translation>Grandigi tiparon se montriĝas mesaĝoj kun nur kelkaj bildosignoj.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Sendi ĉifritajn mesaĝojn nur al kontrolitaj uzantoj</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Postulas, ke uzanto estu kontrolita, por ke ĝi povu ricevi mesaĝojn. Ĉi tio plibonigas sekurecon, sed iom maloportunigas tutvojan ĉifradon.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
-        <translation type="unfinished"></translation>
+        <translation>Havigi ŝlosilojn al kontrolitaj uzantoj kaj aparatoj</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Memage respondas al petoj de aliaj uzantoj je ŝlosiloj, se tiuj uzantoj estas kontrolitaj, eĉ se la aparato ne povus aliri tiujn ŝlosilojn alie.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Enreta savkopiado de ŝlosiloj</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Elŝutu ĉifrajn ŝlosilojn por mesaĝoj de la ĉifrita enreta deponejo de ŝlosiloj, aŭ alŝutu ilin tien.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Ŝalti enretan savkopiadon de ŝlosiloj</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>Aŭtoroj de Nheko rekomendas ne ŝalti enretan savkopiadon de ŝlosiloj, almenaŭ ĝis simetria enreta savkopiado estos disponebla. Ĉu vi tamen volas ĝin ŝalti?</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+253"/>
         <source>CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>KAÅœMEMORITA</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>NOT CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>NE KAÅœMEMORITA</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
-        <translation type="unfinished"></translation>
+        <translation>Skala obligo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the scale factor of the whole user interface.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ŝanĝas skalan obligon de la tuta fasado.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Font size</source>
-        <translation type="unfinished"></translation>
+        <translation>Tipara grando</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Font Family</source>
-        <translation type="unfinished"></translation>
+        <translation>Tipara familio</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Theme</source>
-        <translation type="unfinished"></translation>
+        <translation>HaÅ­to</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Ringtone</source>
-        <translation type="unfinished"></translation>
+        <translation>Sonoro</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set the notification sound to play when a call invite arrives</source>
-        <translation type="unfinished"></translation>
+        <translation>Agordi sciigan sonon, kiu aŭdiĝos je invito al voko</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Microphone</source>
-        <translation type="unfinished"></translation>
+        <translation>Mikrofono</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Filmilo</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera resolution</source>
-        <translation type="unfinished"></translation>
+        <translation>Distingumo de filmilo</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera frame rate</source>
-        <translation type="unfinished"></translation>
+        <translation>Filmerrapido de filmilo</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Allow fallback call assist server</source>
-        <translation type="unfinished"></translation>
+        <translation>Permesi repaŝan asistan servilon por vokoj</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will use turn.matrix.org as assist when your home server does not offer one.</source>
-        <translation type="unfinished"></translation>
+        <translation>Uzos la servilon turn.matrix.org kiel asistanton, kiam via hejma servilo ne disponigos propran.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Device ID</source>
-        <translation type="unfinished"></translation>
+        <translation>Identigilo de aparato</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Device Fingerprint</source>
-        <translation type="unfinished"></translation>
+        <translation>Fingrospuro de aparato</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Ŝlosiloj de salutaĵo</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>IMPORT</source>
-        <translation type="unfinished"></translation>
+        <translation>ENPORTI</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>EXPORT</source>
-        <translation type="unfinished"></translation>
+        <translation>ELPORTI</translation>
     </message>
     <message>
         <location line="-34"/>
         <source>ENCRYPTION</source>
-        <translation type="unfinished"></translation>
+        <translation>ĈIFRADO</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
-        <translation type="unfinished"></translation>
+        <translation>ÄœENERALAJ</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
-        <translation type="unfinished"></translation>
+        <translation>FASADO</translation>
+    </message>
+    <message>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Ludas vidaŭdaĵojn kiel GIF-ojn aŭ WEBP-ojn nur sub musmontrilo.</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
-        <translation type="unfinished"></translation>
+        <translation>Tuŝekrana reĝimo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will prevent text selection in the timeline to make touch scrolling easier.</source>
-        <translation type="unfinished"></translation>
+        <translation>Malhelpos elkton de teksto en historio por faciligi rulumadon per tuŝoj.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Emoji Font Family</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>Bildosigna tiparo</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Ĉefa subskriba ŝlosilo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your most important key. You don&apos;t need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys.</source>
-        <translation type="unfinished"></translation>
+        <translation>Via plej grava ŝlosilo. Vi ne bezonas kaŝmemori ĝin, ĉar de kaŝmemoro ĝi povus esti pli facile ŝtelebla, kaj vi bezonas ĝin nur por ŝanĝado de aliaj viaj subskribaj ŝlosiloj.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>User signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Uzanto-subskriba ŝlosilo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify other users. If it is cached, verifying a user will verify all their devices.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ŝlosilo por kontrolado de aliaj uzantoj. Se ĝi estas kaŝmemorata, kontrolo de uzanto kontrolos ankaŭ ĉiujn ĝiajn aparatojn.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Mem-subskriba ŝlosilo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you.</source>
-        <translation type="unfinished"></translation>
+        <translation>La ŝlosilo por kontrolado de viaj propraj aparatoj. Se ĝi estas kaŝmemorata, kontrolo de unu el viaj aparatoj markos ĝin kontrolita por aliaj viaj aparatoj, kaj por uzantoj, kiuj vin kontrolis.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Backup key</source>
-        <translation type="unfinished"></translation>
+        <translation>Savkopia ŝlosilo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server.</source>
-        <translation type="unfinished"></translation>
+        <translation>La ŝlosilo por malĉifrado de enretaj savkopioj de ŝlosiloj. Se ĝi estas kaŝmemorata, vi povas ŝalti enretan savkopiadon de ŝlosiloj por deponi ŝlosilojn sekure ĉifritajn al la servilo.</translation>
     </message>
     <message>
         <location line="+54"/>
         <source>Select a file</source>
-        <translation type="unfinished">Elektu dosieron</translation>
+        <translation>Elektu dosieron</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -2444,46 +2989,54 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Ĉiuj dosieroj (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
-        <translation type="unfinished"></translation>
+        <translation>Malfermi dosieron kun salutaĵoj</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
         <source>Error</source>
-        <translation type="unfinished"></translation>
+        <translation>Eraro</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
-        <translation type="unfinished"></translation>
+        <translation>Pasvorto de dosiero</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
-        <translation type="unfinished"></translation>
+        <translation>Enigu pasfrazon por malĉifri la dosieron:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
-        <translation type="unfinished"></translation>
+        <translation>La pasvorto ne povas esti malplena</translation>
     </message>
     <message>
         <location line="-8"/>
         <source>Enter passphrase to encrypt your session keys:</source>
-        <translation type="unfinished"></translation>
+        <translation>Enigu pasfrazon por ĉifri ŝlosilojn de via salutaĵo:</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>File to save the exported session keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Dosiero, kien konserviĝos la elportitaj ŝloslioj de salutaĵo</translation>
+    </message>
+</context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Neniu ĉifrita privata babilo kun ĉi tiu uzanto troviĝis. Kreu ĉifritan privatan babilon kun ĉi tiu uzanto kaj reprovu.</translation>
     </message>
 </context>
 <context>
@@ -2491,27 +3044,27 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../qml/device-verification/Waiting.qml" line="+12"/>
         <source>Waiting for other party…</source>
-        <translation type="unfinished"></translation>
+        <translation>Atendante la aliulon…</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Waiting for other side to accept the verification request.</source>
-        <translation type="unfinished"></translation>
+        <translation>Atendante, ĝis la aliulo akceptos la kontrolpeton.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to continue the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Atendante, ĝis la aliulo finos la kontrolon.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to complete the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Atendante, ĝis la aliulo finos la kontrolon.</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
+        <translation>Nuligi</translation>
     </message>
 </context>
 <context>
@@ -2556,7 +3109,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+2"/>
         <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
+        <translation>Nuligi</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -2566,27 +3119,27 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+3"/>
         <source>Topic</source>
-        <translation type="unfinished">Temo</translation>
+        <translation>Temo</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Alias</source>
-        <translation type="unfinished"></translation>
+        <translation>Kromnomo</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Room Visibility</source>
-        <translation type="unfinished"></translation>
+        <translation>Videbleco de ĉambro</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Room Preset</source>
-        <translation type="unfinished"></translation>
+        <translation>Antaŭagordo de ĉambro</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Direct Chat</source>
-        <translation type="unfinished"></translation>
+        <translation>Individua ĉambro</translation>
     </message>
 </context>
 <context>
@@ -2594,53 +3147,22 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/FallbackAuth.cpp" line="+34"/>
         <source>Open Fallback in Browser</source>
-        <translation type="unfinished"></translation>
+        <translation>Iri al foliumilo por la alternativa metodo</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
+        <translation>Nuligi</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Confirm</source>
-        <translation type="unfinished"></translation>
+        <translation>Konfirmi</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Open the fallback, follow the steps and confirm after completing them.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Aliĝi</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation type="unfinished"></translation>
+        <translation>Iru al la alternativa metodo, sekvu la paŝojn, kaj fininte ilin, konfirmu.</translation>
     </message>
 </context>
 <context>
@@ -2648,7 +3170,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/Logout.cpp" line="+35"/>
         <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
+        <translation>Nuligi</translation>
     </message>
     <message>
         <location line="+8"/>
@@ -2661,19 +3183,21 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+29"/>
         <source>Upload</source>
-        <translation type="unfinished"></translation>
+        <translation>Alŝuti</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
+        <translation>Nuligi</translation>
     </message>
     <message>
         <location line="+93"/>
         <source>Media type: %1
 Media size: %2
 </source>
-        <translation type="unfinished"></translation>
+        <translation>Speco de vidaŭdaĵo: %1
+Grandeco de vidaŭdaĵo: %2
+</translation>
     </message>
 </context>
 <context>
@@ -2681,43 +3205,17 @@ Media size: %2
     <message>
         <location filename="../../src/dialogs/ReCaptcha.cpp" line="+35"/>
         <source>Cancel</source>
-        <translation type="unfinished">Nuligi</translation>
+        <translation>Nuligi</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Confirm</source>
-        <translation type="unfinished"></translation>
+        <translation>Konfirmi</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Solve the reCAPTCHA and press the confirm button</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished">Kvitancoj</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished">Fermi</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>HodiaÅ­ %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>HieraÅ­ %1</translation>
+        <translation>Solvu la kontrolon de homeco de «reCAPTCHA» kaj premu la konfirman butonon</translation>
     </message>
 </context>
 <context>
@@ -2725,62 +3223,62 @@ Media size: %2
     <message>
         <location filename="../../src/Utils.h" line="+115"/>
         <source>You sent an audio clip</source>
-        <translation type="unfinished"></translation>
+        <translation>Vi sendis sonmesaĝon</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent an audio clip</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 sendis sonmesaĝon</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Vi sendis bildon</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 sendis bildon</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Vi sendis dosieron</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 sendis dosieron</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Vi sendis filmon</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 sendis filmon</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Vi sendis glumarkon</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 sendis glumarkon</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
-        <translation type="unfinished"></translation>
+        <translation>Vi sendis sciigon</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent a notification</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 sendis sciigon</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -2788,57 +3286,57 @@ Media size: %2
         <translation>Vi: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>You sent an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>Vi sendis ĉifritan mesaĝon</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent an encrypted message</source>
-        <translation type="unfinished">%1 sendis ĉifritan mesaĝon</translation>
+        <translation>%1 sendis ĉifritan mesaĝon</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>You placed a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Vi vokis</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 vokis</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Vi respondis vokon</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 respondis vokon</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Vi finis vokon</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 finis vokon</translation>
     </message>
 </context>
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
-        <translation type="unfinished"></translation>
+        <translation>Nekonata tipo de mesaĝo</translation>
     </message>
 </context>
 </TS>
diff --git a/resources/langs/nheko_es.ts b/resources/langs/nheko_es.ts
index 922e05006122cbd3ce742552eca3ee9ea86935f9..63916a0a29c3b59ff6160c8926f18aefdc4ea480 100644
--- a/resources/langs/nheko_es.ts
+++ b/resources/langs/nheko_es.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Llamando...</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Videollamada</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Videollamada</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Pantalla completa</translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>No se pudo invitar al usuario: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Usuario invitado: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>La migración de la caché a la versión actual ha fallado. Esto puede deberse a distintos motivos. Por favor, reporte el incidente y mientras tanto intente usar una versión anterior. También puede probar a borrar la caché manualmente.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Sala %1 creada.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Confirmar invitación</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -182,7 +182,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Se ha expulsado a %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -227,12 +227,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation type="unfinished"></translation>
     </message>
@@ -247,33 +247,35 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation type="unfinished"></translation>
     </message>
@@ -283,7 +285,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -293,7 +295,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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 filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">Cancelar</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished"></translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">Cancelar</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -756,25 +881,25 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -784,35 +909,53 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -862,18 +1005,23 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -901,7 +1049,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -924,7 +1072,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1073,32 +1233,28 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">No se encontró micrófono.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,27 +1391,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1283,33 +1445,41 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>RoomInfo</name>
     <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1414,16 +1607,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1453,7 +1666,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1468,7 +1686,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1514,12 +1742,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1539,8 +1767,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1548,21 +1776,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1617,6 +1873,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Cancelar</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1669,18 +2040,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1700,7 +2071,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation type="unfinished">
@@ -1709,7 +2080,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1719,7 +2090,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1739,12 +2120,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1754,7 +2135,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1764,7 +2145,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1779,12 +2160,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1814,22 +2200,22 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished">Te has unido a esta sala.</translation>
     </message>
     <message>
-        <location line="+886"/>
+        <location line="+953"/>
         <source>Rejected the knock from %1.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1848,7 +2234,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1856,12 +2242,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1886,28 +2277,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1945,10 +2348,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1958,33 +2386,98 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+29"/>
+        <source>Room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2007,8 +2500,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2016,7 +2509,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2026,22 +2519,22 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2066,7 +2559,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2081,6 +2574,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2109,7 +2612,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2175,7 +2678,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Cancelar</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Cancelar</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2666,32 +3186,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3199,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2760,7 +3254,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2780,27 +3274,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2808,7 +3302,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_et.ts b/resources/langs/nheko_et.ts
index 57eac1c3dfd78d4c7b8933eacc320c8d82dc8fd0..0d72ad6bb411adb6cce08f3a338e88d1f88108f6 100644
--- a/resources/langs/nheko_et.ts
+++ b/resources/langs/nheko_et.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Helistan...</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Videokõne</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Videokõne</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Terve ekraan</translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Kutse saatmine kasutajale ei õnnestunud: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Kutsutud kasutaja: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Puhverdatud andmete muutmine sobivaks rakenduse praeguse versiooniga ei õnnestunud. Sellel võib olla erinevaid põhjuseid. Palun saada meile veateade ja seni kasuta vanemat rakenduse versiooni. Aga kui sa soovid proovida, siis kustuta puhverdatud andmed käsitsi.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Kinnita liitumine</translation>
     </message>
@@ -151,23 +151,23 @@
         <translation>Kas sa kindlasti soovid liituda %1 jututoaga?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>%1 jututuba on loodud.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Kinnita kutse</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Kas sa tõesti soovid saata kutset kasutajale %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Kasutaja %1 kutsumine %2 jututuppa ei õnnestunud: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Kas sa tõesti soovid müksata kasutaja %1 (%2) jututoast välja?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Väljamüksatud kasutaja: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation>Kas sa tõesti soovid kasutajale %1 (%2) seada suhtluskeeldu?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Kasutajale %1 suhtluskeelu seadmine %2 jututoas ei õnnestunud: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation>Kas sa tõesti soovid kasutajalt %1 (%2) eemaldada suhtluskeelu?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Kasutajalt %1 suhtluskeelu eemaldamine %2 jututoas ei õnnestunud: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Suhtluskeeld eemaldatud: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation>Kas sa kindlasti soovid alustada otsevestlust kasutajaga %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Puhvri versiooniuuendus ebaõnnestus!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>Sinu andmekandjale salvestatud puhvri versioon on uuem, kui käesolev Nheko versioon kasutada oskab. Palun tee Nheko uuendus või kustuta puhverdatud andmed.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>OLM konto taastamine ei õnnestunud. Palun logi uuesti sisse.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Salvestatud andmete taastamine ei õnnestunud. Palun logi uuesti sisse.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Krüptovõtmete kasutusele võtmine ei õnnestunud. Koduserveri vastus päringule: %1 %2. Palun proovi hiljem uuesti.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Palun proovi uuesti sisse logida: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Jututoaga liitumine ei õnnestunud: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Sa liitusid selle jututoaga</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Kutse tagasivõtmine ei õnnestunud: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Jututoa loomine ei õnnestunud: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Jututoast lahkumine ei õnnestunud: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation>Kasutaja %1 väljamüksamine %2 jututoast ei õnnestunud: %3</translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Dekrüpti andmed</translation>
     </message>
@@ -362,12 +364,12 @@
         <translation>Andmete dekrüptimiseks sisesta oma taastevõti või salafraas:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation>Andmete dekrüptimiseks sisesta oma taastevõti või salafraas nimega %1:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Dekrüptimine ei õnnestunud</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Otsi</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Inimesed</translation>
     </message>
@@ -495,71 +497,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>See sõnum on krüptimata!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Selle sõnumi dekrüptimiseks pole veel vajalikke võtmeid. Me oleme neid serverist automaatselt laadimas, kuid kui sul on väga kiire, siis võid seda uuesti teha.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation>Krüptitud verifitseeritud seadmes</translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Meil on krüptovõtmed vaid uuemate sõnumite jaoks ja seda sõnumit ei saa dekrüptida. Sa võid proovida vajalikke võtmeid eraldi laadida.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation>Krüptitud verifitseerimata seadmes, aga sa oled selle kasutajat seni usaldanud.</translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Krüptovõtmete andmekogust lugemisel tekkis rakenduses viga.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation>Krüptitud verifitseerimata seadmes</translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Sõnumi dekrüptimisel tekkis viga.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>-- Krüptitud sündmus (Dekrüptimisvõtmeid ei leidunud) --</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Sõnumi töötlemisel tekkis viga.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation>-- Krüptitud sündmus (võti pole selle indeksi jaoks sobilik) --</translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>Krüptovõtit on kasutatud korduvalt! Keegi võib proovida siia vestlusesse valesõnumite lisamist!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>-- Dekrüptimise viga (megolm&apos;i võtmete laadimine andmebaasist ei õnnestunud) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Teadmata viga dekrüptimisel</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Dekrüptimise viga (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Laadi krüptovõti</translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>-- Krüptitud sündmus (Tundmatu sündmuse tüüp) --</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>See sõnum on krüptimata!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Kordusel põhinev rünne! Selle sõnumi indeksit on uuesti kasutatud! --</translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Krüptitud verifitseeritud seadmes</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Sõnum verifitseerimata seadmest! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Krüptitud verifitseerimata seadmes, aga sa oled selle kasutajat seni usaldanud.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Krüptitud verifitseerimata seadme poolt või krüptovõtmed on pärit allikast, mida sa pole üheselt usaldanud (näiteks varundatud võtmed).</translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Seadme verifitseerimine aegus.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Teine osapool katkestas verifitseerimise.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Sulge</translation>
     </message>
@@ -604,48 +613,138 @@
         <translation>Suuna sõnum edasi</translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Muudan pildipakki</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Lisa pilte</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Kleepsud (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>Olekuvõti</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Pildikogu nimi</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Viide allikale</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Kasuta emojina</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Kasuta kleepsuna</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Lühend</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Sisu</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Eemalda pakist</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Eemalda</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Loobu</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Salvesta</translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Pildikogu seadistused</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Losa kasutajakontokohane pildipakk</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Uus jututoa pildipakk</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Isiklik pildipakk</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Pildipakk sellest jututoast</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Üldkasutatav pildipakk</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Luba kasutada üldiselt</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Sellega võimaldad pildipaki kasutamist kõikides jututubades</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Muuda</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished">Sulge</translation>
+        <translation>Sulge</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Vali fail</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation>Kõik failid (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Meediafailide üleslaadimine ei õnnestunud. Palun proovi uuesti.</translation>
     </message>
@@ -663,36 +762,62 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsu kasutajaid %1 jututuppa</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>User ID to invite</source>
-        <translation type="unfinished">Kasutajatunnus, kellele soovid kutset saata</translation>
+        <translation>Kasutajatunnus, kellele soovid kutset saata</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@kadri:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>Lisa</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Saada kutse</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
+        <translation>Loobu</translation>
+    </message>
+</context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Jututoa tunnus või alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Lahku jututoast</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Kas sa oled kindel, et soovid lahkuda?</translation>
+    </message>
 </context>
 <context>
     <name>LoginPage</name>
@@ -760,25 +885,25 @@ Näiteks: https://server.minu:8787</translation>
         <translation>LOGI SISSE</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation>Sisestatud Matrix&apos;i kasutajatunnus on vigane - peaks olema @kasutaja:server.tld</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Koduserveri automaatne tuvastamine ei õnnestunud: päringuvastus oli vigane.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Koduserveri automaatne tuvastamine ei õnnestunud: tundmatu viga .well-known päringu tegemisel.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Protokolli järgi nõutavaid lõpppunkte ei leidunud. Ilmselt pole tegemist Matrix&apos;i serveriga.</translation>
     </message>
@@ -788,35 +913,53 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Päringule sain tagasi vigase vastuse. Palun kontrolli, et koduserveri domeen oleks õige.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Tekkis teadmata viga. Palun kontrolli, et koduserveri domeen on õige.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>ÜHEKORDNE SISSELOGIMINE</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Tühi salasõna</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>Ühekordne sisselogimine ei õnnestunud</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation>Krüptimine on kasutusel</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>jututoa uus nimi on: %1</translation>
     </message>
@@ -866,18 +1009,23 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Ühendan kõnet…</translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Luba neid</translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation>%1 vastas kõnele.</translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>eemaldatud</translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation>%1 lõpetas kõne.</translation>
     </message>
@@ -905,9 +1053,9 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Kirjuta sõnum…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>Kleepsud</translation>
     </message>
     <message>
         <location line="+24"/>
@@ -928,7 +1076,7 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Muuda</translation>
     </message>
@@ -948,17 +1096,19 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Valikud</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation>&amp;Kopeeri</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation>Kopeeri &amp;lingi asukoht</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation>Re&amp;ageeri</translation>
     </message>
@@ -1017,6 +1167,11 @@ Näiteks: https://server.minu:8787</translation>
         <source>Copy link to eve&amp;nt</source>
         <translation>Kopeeri sündmuse li&amp;nk</translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>&amp;Vaata tsiteeritud sõnumit</translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Saabus verifitseerimispäring</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation>Selleks, et muud kasutajad automaatselt usaldaks sinu seadmeid, peaksid nad verifitseerima. Samaga muutub ka krüptovõtmete varundus automaatseks. Kas verifitseerime seadme %1?</translation>
     </message>
@@ -1076,33 +1236,29 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Nõustu</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 saatis krüptitud sõnumi</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation>* %1 %2</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>%1 vastas: %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation>%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,7 +1289,7 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Ei suuda tuvastada mikrofoni.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Häälkõne</translation>
     </message>
@@ -1164,7 +1320,7 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation>Loo unikaalne profiil, mis võimaldab sul logida samaaegselt sisse erinevatele kasutajakontodele ning käivitada mitu Nheko programmiakent.</translation>
     </message>
@@ -1179,21 +1335,37 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Profiili nimi</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Lugemisteatised</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Eile, %1</translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Kasutajanimi</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Kasutajanimi ei tohi olla tühi ning võib sisaldada vaid a-z, 0-9, ., _, =, -, / tähemärke.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Salasõna</translation>
     </message>
@@ -1213,7 +1385,7 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Koduserver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>See on server, kus sa oma kasutajakonto registreerid. Kuna Matrix on hajutatud suhtlusvõrk, siis esmalt pead leidma sulle sobiliku koduserveri või panema püsti täitsa oma enda koduserveri.</translation>
     </message>
@@ -1223,27 +1395,17 @@ Näiteks: https://server.minu:8787</translation>
         <translation>REGISTREERI</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Selline registreerimise töövoog pole toetatud!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>Ühel või enamal andmeväljal on vigane väärtus. Palun paranda vead ja proovi uuesti.</translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Koduserveri automaatne tuvastamine ei õnnestunud: päringuvastus oli vigane.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Koduserveri automaatne tuvastamine ei õnnestunud: tundmatu viga .well-known päringu tegemisel.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Protokolli järgi nõutavaid lõpppunkte ei leidunud. Ilmselt pole tegemist Matrix&apos;i serveriga.</translation>
     </message>
@@ -1258,17 +1420,17 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Tekkis teadmata viga. Palun kontrolli, et koduserveri domeen on õige.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Salasõna pole piisavalt pikk (vähemalt 8 tähemärki)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Salasõnad ei klapi omavahel</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Vigane koduserveri nimi</translation>
     </message>
@@ -1276,7 +1438,7 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Sulge</translation>
     </message>
@@ -1286,10 +1448,28 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Tühista muudatused</translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Tutvu avalike jututubadega</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Otsi avalikke jututube</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation>salvestatud versiooni ei leidu</translation>
     </message>
@@ -1297,7 +1477,7 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation>Uus silt</translation>
     </message>
@@ -1306,16 +1486,6 @@ Näiteks: https://server.minu:8787</translation>
         <source>Enter the tag you want to use:</source>
         <translation>Kirjuta silt, mida soovid kasutada:</translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1347,7 +1517,7 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Loo uus silt...</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation>Olekuteade</translation>
     </message>
@@ -1367,12 +1537,35 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Sisesta olekuteade</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation>Logi välja</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Sulge</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation>Alusta uut vestlust</translation>
     </message>
@@ -1392,7 +1585,7 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Jututubade loend</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation>Kasutaja seadistused</translation>
     </message>
@@ -1400,34 +1593,54 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 jututoa liikmed</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%n osaline %1 jututoas</numerusform>
+            <numerusform>%n osalist %1 jututoas</numerusform>
         </translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Invite more people</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsu veel liikmeid</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>See jututuba on krüptimata!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>See kasutaja on verifitseeritud.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>See kasutaja ei ole verifitseeritud, kuid ta kasutab jätkuvalt krüpto jaoks juurvõtmeid sellest ajast, kui te kohtusite.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Sellel kasutajal on verifitseerimata seadmeid!</translation>
     </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation>Jututoa seadistused</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation>%1 liige(t)</translation>
     </message>
@@ -1457,7 +1670,12 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Kõik sõnumid</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Ligipääs jututuppa</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation>Kõik (sealhulgas külalised)</translation>
     </message>
@@ -1472,7 +1690,17 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Kutsutud kasutajad</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>Koputades</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Piiratud teiste jututubade liikmelisusega</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Krüptimine</translation>
     </message>
@@ -1490,17 +1718,17 @@ Näiteks: https://server.minu:8787</translation>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Kleepsude ja emotikonide seadistused</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Muuda</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Muuda missugused efektipakid on kasutusel, eemalda neid ja loo uusi</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -1518,12 +1746,12 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Jututoa versioon</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation>Krüptimise kasutuselevõtmine ei õnnestunud: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Vali tunnuspilt</translation>
     </message>
@@ -1543,8 +1771,8 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Viga faili lugemisel: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation>Viga faili üleslaadimisel: %1</translation>
     </message>
@@ -1552,18 +1780,46 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ootel kutse.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Jututoa eelvaade</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
+        <translation>Eelvaade pole saadaval</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1621,6 +1877,121 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Loobu</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Ühenduse loomine võtmehoidlaga ei õnnestunud</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Pildipaki uuendamine ei õnnestunud: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Vana pildipaki kustutamine ei õnnestunud: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Pildi avamine ei õnnestunud: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Faili üleslaadimine ei õnnestunud: %1</translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1649,7 +2020,7 @@ Näiteks: https://server.minu:8787</translation>
     <message>
         <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
         <source>Search</source>
-        <translation type="unfinished">Otsi</translation>
+        <translation>Otsi</translation>
     </message>
 </context>
 <context>
@@ -1673,18 +2044,18 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Sõnumi ümbersõnastamine ebaõnnestus: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation>Sündmuse krüptimine ei õnnestunud, katkestame saatmise!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Salvesta pilt</translation>
     </message>
@@ -1704,7 +2075,7 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Salvesta fail</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1713,7 +2084,7 @@ Näiteks: https://server.minu:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 tegi jututoa avalikuks.</translation>
     </message>
@@ -1723,7 +2094,17 @@ Näiteks: https://server.minu:8787</translation>
         <translation>%1 seadistas, et selle jututoaga liitumine eeldab kutset.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 pääses jututuppa peale uksele koputamist.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 lubas järgmiste jututubade liikmetel selle jututoaga liituda: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 muutis selle jututoa külalistele ligipääsetavaks.</translation>
     </message>
@@ -1743,12 +2124,12 @@ Näiteks: https://server.minu:8787</translation>
         <translation>%1 muutis, et selle jututoa ajalugu saavad lugeda kõik liikmed alates praegusest ajahetkest.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 muutis, et selle jututoa ajalugu saavad lugeda kõik liikmed alates oma kutse saatmisest.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 muutis, et selle jututoa ajalugu saavad lugeda kõik liikmed alates jututoaga liitumise hetkest.</translation>
     </message>
@@ -1758,12 +2139,12 @@ Näiteks: https://server.minu:8787</translation>
         <translation>%1 muutis selle jututoa õigusi.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 sai kutse.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 muutis oma tunnuspilti.</translation>
     </message>
@@ -1773,12 +2154,17 @@ Näiteks: https://server.minu:8787</translation>
         <translation>%1 muutis oma profiili.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 liitus jututoaga.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 liitus peale autentimist serverist %2.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 lükkas liitumiskutse tagasi.</translation>
     </message>
@@ -1808,32 +2194,32 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Kasutaja %1 sai suhtluskeelu.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation>Põhjus: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 muutis oma koputust jututoa uksele.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Sa liitusid jututoaga.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation>%1 muutis oma tunnuspilti ja seadistas uueks kuvatavaks nimeks %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation>%1 seadistas uueks kuvatavaks nimeks %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Lükkas tagasi %1 koputuse jututoa uksele.</translation>
     </message>
@@ -1852,7 +2238,7 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Muudetud</translation>
     </message>
@@ -1860,29 +2246,34 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Ühtegi jututuba pole avatud</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Eelvaade pole saadaval</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation>%1 liige(t)</translation>
     </message>
     <message>
         <location line="+33"/>
         <source>join the conversation</source>
-        <translation type="unfinished"></translation>
+        <translation>liitu vestlusega</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>accept invite</source>
-        <translation type="unfinished"></translation>
+        <translation>võta kutse vastu</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>decline invite</source>
-        <translation type="unfinished"></translation>
+        <translation>lükka kutse tagasi</translation>
     </message>
     <message>
         <location line="+27"/>
@@ -1890,28 +2281,40 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Tagasi jututubade loendisse</translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation>Ühtegi krüptitud vestlust selle kasutajaga ei leidunud. Palun loo temaga krüptitud vestlus ja proovi uuesti.</translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation>Tagasi jututubade loendisse</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation>Jututuba on valimata</translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>See jututuba on krüptimata!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Selles jututoas on vaid verifitseeritud seadmed.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Selles jututoas leidub verifitseerimata seadmeid!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation>Jututoa valikud</translation>
     </message>
@@ -1949,10 +2352,35 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Lõpeta töö</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Registreerimiseks palun sisesta kehtiv tunnusluba.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation>Üldine kasutajaprofiil</translation>
     </message>
@@ -1962,33 +2390,98 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Kasutajaprofiil jututoas</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Muuda oma tunnuspilti kõikjal.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Muuda oma tunnuspilti vaid selles jututoas.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Muuda oma kuvatavat nime kõikjal.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Muuda oma kuvatavat nime vaid selles jututoas.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Jututuba: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>See kasutajaprofiil on vaid selle jututoa kohane. Kasutaja kuvatav nimi ja tunnuspilt võivad muudes jutubades olla teistsugused.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Vaata selle kasutaja üldist profiili.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
         <translation>Verifitseeri</translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
-        <translation>Sea kasutajale suhtluskeeld</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Alusta privaatset vestlust.</translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation>Alusta privaatset vestlust</translation>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Müksa kasutaja välja.</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
-        <translation>Müksa kasutaja välja</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Sea kasutajale suhtluskeeld.</translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation>Võta verifitseerimine tagasi</translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Vali tunnuspilt</translation>
     </message>
@@ -2011,8 +2504,8 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation>Vaikimisi</translation>
     </message>
@@ -2020,7 +2513,7 @@ Näiteks: https://server.minu:8787</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Vähenda tegumiribale</translation>
     </message>
@@ -2030,22 +2523,22 @@ Näiteks: https://server.minu:8787</translation>
         <translation>Käivita tegumiribalt</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Rühmade küljepaan</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Ümmargused tunnuspildid</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation>Profiil: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation>Vaikimisi</translation>
     </message>
@@ -2070,7 +2563,7 @@ Näiteks: https://server.minu:8787</translation>
         <translation>ALLALAADIMISED</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation>Peale akna sulgemist jäta rakendus taustal tööle.</translation>
     </message>
@@ -2086,6 +2579,16 @@ OFF - square, ON - Circle.</source>
         <translation>Muuda vestlustes kuvatavate tunnuspiltide kuju.
 Väljalülitatuna - ruut, sisselülitatuna - ümmargune.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2116,7 +2619,7 @@ be blurred.</source>
 siis ajajoone vaade hägustub.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation>Viivitus privaatsussirmi sisselülitamisel (sekundites [0 - 3600])</translation>
     </message>
@@ -2176,7 +2679,7 @@ Kui see valik on välja lülitatud, siis jututoad järjestatakse viimati saanunu
 Kui see valik on sisse lülitatud, siis teavitustega jututoad (pisike ümmargune numbrifa ikoon) järjestatakse esimesena. Sinu poolt summutatud jututoad järjestatakse ikkagi ajatempli alusel, sest sa ei pea neid teistega võrreldes piisavalt tähtsaks.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Lugemisteatised</translation>
     </message>
@@ -2188,7 +2691,7 @@ Status is displayed next to timestamps.</source>
 Lugemise olekut kuvatakse ajatempli kõrval.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Saada sõnumid Markdown-vormindusena</translation>
     </message>
@@ -2201,6 +2704,11 @@ Kui Markdown ei ole kasutusel, siis saadetakse kõik sõnumid vormindamata tekst
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Esita liikuvaid pilte vaid siis, kui kursor on pildi kohal</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Töölauakeskkonna teavitused</translation>
     </message>
@@ -2242,12 +2750,47 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Tee sõnumi font suuremaks, kui sõnumis on vaid mõned emojid.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Saada krüptitud sõnumeid vaid verifitseeritud kasutajatele</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Selle tingimuse alusel peab kasutaja olema krüptitud sõnumivahetuse jaoks verifitseeritud. Niisugune nõue parandab turvalisust, kuid teeb läbiva krüptimise natuke ebamugavamaks.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation>Jaga võtmeid verifitseeritud kasutajate ja seadmetega</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Kui teised kasutajad on verifitseeritud, siis luba automaatselt vastata nende krüptovõtmete päringutele isegi siis, kui too seade ei peaks saama neid võtmeid kasutada.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Krüptovõtmete varundus võrgus</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Luba krüptitud võtmete varunduseks laadida sõnumite krüptovõtmeid sinu serverisse või sinu serverist.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Võta kasutusele krüptovõtmete varundus võrgus</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>Seni kuni sümmetriline krüptovõtmete varundamine pole teostatav, siis Nheko arendajad ei soovita krüptovõtmeid võrgus salvestada. Kas ikkagi jätkame?</translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation>PUHVERDATUD</translation>
     </message>
@@ -2257,7 +2800,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>PUHVERDAMATA</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Mastaabitegur</translation>
     </message>
@@ -2332,7 +2875,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Seadme sõrmejälg</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Sessioonivõtmed</translation>
     </message>
@@ -2352,17 +2895,22 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>KRÜPTIMINE</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>ÜLDISED SEADISTUSED</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>LIIDES</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Esitame liikuvaid GIF ja WEBP pilte vaid siis, kui kursor on pildi kohal.</translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation>Puuteekraani režiim</translation>
     </message>
@@ -2377,14 +2925,9 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Fondiperekond emojide jaoks</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation>Vasta verifitseeritud kasutajate krüptovõtmete päringutele automaatselt.</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
-        <translation>Üldine allkirjavõti</translation>
+        <translation>Allkirjastamise juurvõti</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2402,7 +2945,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Teiste kasutajate verifitseerimiseks mõeldud võti. Kui see võti on puhverdatud, siis kasutaja verifitseerimine tähendab ka kõikide tema seadmete verifitseerimist.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation>Omatunnustusvõti</translation>
     </message>
@@ -2432,14 +2975,14 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Kõik failid (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Ava sessioonide fail</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2447,19 +2990,19 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Viga</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Faili salasõna</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Faili dekrüptimiseks sisesta salafraas:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>Salasõna ei saa olla tühi</translation>
     </message>
@@ -2474,6 +3017,14 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Fail, kuhu salvestad eksporditavad sessiooni krüptovõtmed</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Ühtegi krüptitud vestlust selle kasutajaga ei leidunud. Palun loo temaga krüptitud vestlus ja proovi uuesti.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2599,37 +3150,6 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim
         <translation>Ava kasutaja registreerimise tagavaravariant, läbi kõik sammud ja kinnita seda, kui kõik valmis on.</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Liitu</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Tühista</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Jututoa tunnus või alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Tühista</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Kas sa oled kindel, et soovid lahkuda?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2683,32 +3203,6 @@ Meedia suurus: %2
         <translation>Vasta reCAPTCHA küsimustele ja vajuta kinnita-nuppu</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Lugemisteatised</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Sulge</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Täna %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Eile %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2722,47 +3216,47 @@ Meedia suurus: %2
         <translation>%1 saatis helifaili</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Sa saatsid pildi</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 saatis pildi</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Sa saatsid faili</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 saatis faili</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Sa saatsid video</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 saatis video</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Sa saatsid kleepsu</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 saatis kleepsu</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Sa saatsid teavituse</translation>
     </message>
@@ -2777,7 +3271,7 @@ Meedia suurus: %2
         <translation>Sina: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2797,27 +3291,27 @@ Meedia suurus: %2
         <translation>Sa helistasid</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 helistas</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Sa vastasid kõnele</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 vastas kõnele</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Sa lõpetasid kõne</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 lõpetas kõne</translation>
     </message>
@@ -2825,7 +3319,7 @@ Meedia suurus: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Tundmatu sõnumitüüp</translation>
     </message>
diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts
index 422c29570e58fe6be08ebad7a8edcd206f716598..2fb5221d2ea7821629278a50e89178c1b0c8244f 100644
--- a/resources/langs/nheko_fi.ts
+++ b/resources/langs/nheko_fi.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Soitetaan...</translation>
     </message>
@@ -22,7 +22,7 @@
     <message>
         <location line="+17"/>
         <source>Hide/Show Picture-in-Picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Piilota/Näytä kuva kuvassa</translation>
     </message>
     <message>
         <location line="+13"/>
@@ -45,7 +45,7 @@
     <message>
         <location line="+12"/>
         <source>Waiting for other side to complete verification.</source>
-        <translation type="unfinished"></translation>
+        <translation>Odotetaan toista puolta saamaan vahvistus loppuun.</translation>
     </message>
     <message>
         <location line="+13"/>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Videopuhelu</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Videopuhelu</translation>
     </message>
@@ -117,31 +117,31 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Koko näyttö</translation>
     </message>
 </context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Käyttäjää %1 ei onnistuttu kutsumaan</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Kutsuttu käyttäjä: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
-        <translation type="unfinished"></translation>
+        <translation>Välimuistin tuominen nykyiseen versioon epäonnistui. Tällä voi olla eri syitä. Luo vikailmoitus ja yritä sillä aikaa käyttää vanhempaa versiota. Voit myös vaihtoehtoisesti koettaa tyhjentää välimuistin käsin.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Vahvista liittyminen</translation>
     </message>
@@ -151,139 +151,141 @@
         <translation>Haluatko todella liittyä huoneeseen %1?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
-        <translation type="unfinished"></translation>
+        <translation>Huone %1 luotu.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvista kutsu</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Haluatko kutsua %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Epäonnistuttiin kutsuminen %1 huoneeseen %2:%3</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Confirm kick</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvista potkut</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to kick %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Haluatko potkia %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Potkittiin käyttäjä: %1</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Confirm ban</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvista porttikielto</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to ban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Haluatko antaa porttikiellon käyttäjälle %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Ei onnistuttu antamaan porttikieltoa käyttäjälle %1 huoneessa %2:%3</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Banned user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Annettiin porttikielto käyttäjälle: %1</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Confirm unban</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvista porttikiellon purku</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to unban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Haluatko purkaa porttikiellon käyttäjältä %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Ei onnistuttu purkamaan porttikieltoa käyttäjältä %1 huoneessa %2: %3</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Unbanned user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Purettiin porttikielto käyttäjältä %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Haluatko luoda yksityisen keskustelun käyttäjän %1 kanssa?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
-        <translation type="unfinished"></translation>
+        <translation>Välimuistin siirto epäonnistui!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Incompatible cache version</source>
-        <translation type="unfinished"></translation>
+        <translation>Yhteensopimaton välimuistin versio</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache.</source>
-        <translation type="unfinished"></translation>
+        <translation>Levylläsi oleva välimuisti on uudempaa kuin mitä tämä Nhekon versio tukee. Päivitä tai poista välimuistisi.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <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>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <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="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Ole hyvä ja yritä kirjautua sisään uudelleen: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Huoneeseen liittyminen epäonnistui: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Sinä liityit huoneeseen</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Failed to remove invite: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsua ei onnistuttu poistamaan: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Huoneen luominen epäonnistui: %1</translation>
     </message>
@@ -293,9 +295,9 @@
         <translation>Huoneesta poistuminen epäonnistui: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Ei onnistuttu potkimaan käyttäjää %1 huoneesta %2: %3</translation>
     </message>
 </context>
 <context>
@@ -303,7 +305,7 @@
     <message>
         <location filename="../qml/CommunitiesList.qml" line="+44"/>
         <source>Hide rooms with this tag or from this space by default.</source>
-        <translation type="unfinished"></translation>
+        <translation>Piilota huoneet tällä tagilla tai tästä tilasta oletuksena.</translation>
     </message>
 </context>
 <context>
@@ -316,65 +318,65 @@
     <message>
         <location line="+2"/>
         <source>Shows all rooms without filtering.</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä kaikki huoneet ilman suodattamista.</translation>
     </message>
     <message>
         <location line="+30"/>
         <source>Favourites</source>
-        <translation type="unfinished"></translation>
+        <translation>Suosikit</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms you have favourited.</source>
-        <translation type="unfinished"></translation>
+        <translation>Suosikkihuoneesi.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Low Priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Matala tärkeysjärjestys</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms with low priority.</source>
-        <translation type="unfinished"></translation>
+        <translation>Huoneet matalalla tärkeysjärjestyksellä.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Server Notices</source>
-        <translation type="unfinished"></translation>
+        <translation>Palvelimen ilmoitukset</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Messages from your server or administrator.</source>
-        <translation type="unfinished"></translation>
+        <translation>Viestit palvelimeltasi tai ylläpitäjältä.</translation>
     </message>
 </context>
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
-        <translation type="unfinished"></translation>
+        <translation>Salaisuuksien salauksen purku</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Enter your recovery key or passphrase to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Anna palauttamisavain tai salasana purkaaksesi salaisuuksiesi salaus:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Anna palautusavaimesi tai salasanasi nimeltä %1 purkaaksesi salaisuuksien salauksen:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Salauksen purku epäonnistui</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Failed to decrypt secrets with the provided recovery key or passphrase</source>
-        <translation type="unfinished"></translation>
+        <translation>Salaisuuksien salauksen purkaminen ei onnistunut annetulla palautusavaimella tai salasanalla</translation>
     </message>
 </context>
 <context>
@@ -382,22 +384,22 @@
     <message>
         <location filename="../qml/device-verification/DigitVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvistuskoodi</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Please verify the following digits. You should see the same numbers on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvista seuraavat numerot. Sinun tulisi nähdä samat numerot molemmilla puolilla. Jos niissä on eroa, paina &quot;Ne eivät vastaa toisiaan&quot; peruaksesi vahvistuksen!</translation>
     </message>
     <message>
         <location line="+31"/>
         <source>They do not match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ne eivät vastaa toisiaan!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>They match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ne vastaavat toisiaan!</translation>
     </message>
 </context>
 <context>
@@ -431,7 +433,7 @@
         <translation>Hae</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Ihmiset</translation>
     </message>
@@ -476,90 +478,88 @@
     <message>
         <location filename="../qml/device-verification/EmojiVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvistuskoodi</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvista seuraava emoji. Sinun tulisi nähdä sama emoji molemmilla puolilla. Jos ne eroavat toisistaan, paina &quot;Ne eivät vastaa toisiaan&quot; peruaksesi vahvistuksen!</translation>
     </message>
     <message>
         <location line="+376"/>
         <source>They do not match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ne eivät vastaa toisiaan!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>They match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ne vastaavat toisiaan!</translation>
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Tätä viestiä ei ole salattu!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Tämän viestin avaamista varten ei ole avainta. Pyysimme avainta automaattisesti, mutta voit yrittää pyytää sitä uudestaan jos olet kärsimätön.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Tämän viestin salausta ei voitu purkaa, koska meillä on avain vain uudemmille viesteille. Voit yrittää pyytää pääsyä tähän viestiin.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation type="unfinished"></translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Sisäinen virhe tapahtui kun salausavainta yritettiin lukea tietokannasta.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation type="unfinished"></translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Tämän viestin salauksen purkamisessa tapahtui virhe.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Tätä viestiä ei voitu jäsentää.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>Salausavainta käytettiin uudelleen! Joku yrittää mahdollisesti tuoda vääriä viestejä tähän keskusteluun!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Tuntematon virhe salauksen purkamisessa</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Virhe purkaessa salausta (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Pyydä avainta</translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Tätä viestiä ei ole salattu!</translation>
     </message>
     <message>
-        <location line="-52"/>
-        <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>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Vahvistetun laitteen salaama</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Vahvistamattoman laitteen salama, mutta olet luottanut tähän asti tuohon käyttäjään.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Vahvistamattoman laitteen salaama tai tämä avain on epäluotettavasta lähteestä kuten avaimen varmuuskopiosta.</translation>
     </message>
 </context>
 <context>
@@ -567,31 +567,40 @@
     <message>
         <location filename="../qml/device-verification/Failed.qml" line="+11"/>
         <source>Verification failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Vahvistus epäonnistui</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Other client does not support our verification protocol.</source>
-        <translation type="unfinished"></translation>
+        <translation>Toinen asiakasohjelma ei tue vahvistusprotokollaamme.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Key mismatch detected!</source>
-        <translation type="unfinished"></translation>
+        <translation>Tunnistettiin virheellinen avain!</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
-        <translation type="unfinished"></translation>
+        <translation>Aikakatkaisu laitteen vahvistuksessa.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
+        <translation>Toinen osapuoli perui vahvistuksen.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Sulje</translation>
     </message>
@@ -604,48 +613,138 @@
         <translation>Välitä viesti</translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Muokataan kuvapakkausta</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Lisää kuvia</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Tarrat (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>TIla-avain</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Pakkauksen nimi</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Osoitus</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Käytä emojina</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Käytä tarrana</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Lyhyt koodi</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Runko</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Poista pakkauksesta</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Poista</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Peruuta</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Tallenna</translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Kuvapakkauksen asetukset</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Luo tilipakkaus</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Uusi huonepakkaus</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Yksityinen pakkaus</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Pakkaus tälle huoneelle</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Kaikkialla käytössä oleva pakkaus</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Salli käytettäväksi kaikkialla</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Sallii tämän pakkauksen käytettäväksi kaikissa huoneissa</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Muokkaa</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished">Sulje</translation>
+        <translation>Sulje</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Valitse tiedosto</translation>
     </message>
@@ -655,43 +754,69 @@
         <translation>Kaikki Tiedostot (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Mediaa ei onnistuttu lataamaan. Yritä uudelleen.</translation>
     </message>
 </context>
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsu käyttäjiä %1</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>User ID to invite</source>
-        <translation type="unfinished">Käyttäjätunnus kutsuttavaksi</translation>
+        <translation>Käyttäjätunnus kutsuttavaksi</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@matti:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>Lisää</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsu</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Peruuta</translation>
+        <translation>Peruuta</translation>
+    </message>
+</context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Huoneen tunnus tai osoite</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Poistu huoneesta</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Oletko varma, että haluat poistua?</translation>
     </message>
 </context>
 <context>
@@ -712,7 +837,10 @@
 You can also put your homeserver address there, if your server doesn&apos;t support .well-known lookup.
 Example: @user:server.my
 If Nheko fails to discover your homeserver, it will show you a field to enter the server manually.</source>
-        <translation type="unfinished"></translation>
+        <translation>Kirjautumisnimesi. MXID:n pitäisi alkaa @ -merkillä, jota seuraa käyttäjätunnus. Käyttäjätunnuksen jälkeen sinun pitää antaa palvelimen nimi kaksoispisteen (:) jälkeen.
+Voit myös laittaa tähän kotipalvelimesi osoitteen, jos palvelimesi ei tunne etsintää.
+Esimerkki: @user:server.my
+Jos Nheko ei onnistu löytämään kotipalvelintasi, se näyttää sinulle kentän, johon laittaa palvelin käsin.</translation>
     </message>
     <message>
         <location line="+25"/>
@@ -732,7 +860,7 @@ If Nheko fails to discover your homeserver, it will show you a field to enter th
     <message>
         <location line="+2"/>
         <source>A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.</source>
-        <translation type="unfinished"></translation>
+        <translation>Tämän laitteen nimi, joka näytetään muille kun laitteitasi vahvistetaan. Oletusta käytetään jos mitään ei anneta.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -742,13 +870,14 @@ If Nheko fails to discover your homeserver, it will show you a field to enter th
     <message>
         <location line="+1"/>
         <source>server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>server.my:8787</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The address that can be used to contact you homeservers client API.
 Example: https://server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>Osoite, jota voidaan käyttää ottamaan yhteyttä kotipalvelimesi asiakasrajapintaan.
+Esimerkki: https://server.my:8787</translation>
     </message>
     <message>
         <location line="+19"/>
@@ -756,25 +885,25 @@ Example: https://server.my:8787</source>
         <translation>KIRJAUDU</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
-        <translation type="unfinished"></translation>
+        <translation>Väärä Matrix-tunnus. Esim.  @joe:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Palvelimen tietojen hakeminen epäonnistui: virheellinen vastaus.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Palvelimen tietojen hakeminen epäonnistui: tuntematon virhe hakiessa .well-known -tiedostoa.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Vaadittuja päätepisteitä ei löydetty. Mahdollisesti ei Matrix-palvelin.</translation>
     </message>
@@ -784,33 +913,51 @@ Example: https://server.my:8787</source>
         <translation>Vastaanotettiin virheellinen vastaus. Varmista, että kotipalvelimen osoite on pätevä.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Tapahtui tuntematon virhe. Varmista, että kotipalvelimen osoite on pätevä.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation>SSO-kirjautuminen</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Tyhjä salasana</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
+        <translation>SSO-kirjautuminen epäonnistui</translation>
+    </message>
+</context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
-        <translation type="unfinished"></translation>
+        <translation>poistettu</translation>
     </message>
     <message>
         <location line="+9"/>
@@ -818,44 +965,44 @@ Example: https://server.my:8787</source>
         <translation>Salaus on käytössä</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>huoneen nimi muutettu: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed room name</source>
-        <translation type="unfinished"></translation>
+        <translation>poistettu huoneen nimi</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>topic changed to: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>aihe vaihdettiin: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed topic</source>
-        <translation type="unfinished"></translation>
+        <translation>poistettu aihe</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 changed the room avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 muutti huoneen avataria</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 created and configured room: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 loi ja sääti huoneen: %2</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>%1 placed a voice call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 asetti äänipuhelun.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 placed a video call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 laittoi videopuhelun.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -870,12 +1017,17 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+12"/>
         <source>%1 ended the call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 päätti puhelun.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Negotiating call...</source>
-        <translation type="unfinished"></translation>
+        <translation>Neuvotellaan puhelua.…</translation>
+    </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Salli heidät sisään</translation>
     </message>
 </context>
 <context>
@@ -883,12 +1035,12 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/MessageInput.qml" line="+44"/>
         <source>Hang up</source>
-        <translation type="unfinished"></translation>
+        <translation>Punainen luuri</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Place a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Soita puhelu</translation>
     </message>
     <message>
         <location line="+25"/>
@@ -901,9 +1053,9 @@ Example: https://server.my:8787</source>
         <translation>Kirjoita viesti…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>Tarrat</translation>
     </message>
     <message>
         <location line="+24"/>
@@ -913,18 +1065,18 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+16"/>
         <source>Send</source>
-        <translation type="unfinished"></translation>
+        <translation>Lähetä</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>You don&apos;t have permission to send messages in this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Sinulla ei ole lupaa lähettää viestejä tässä huoneessa</translation>
     </message>
 </context>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Muokkaa</translation>
     </message>
@@ -944,74 +1096,81 @@ Example: https://server.my:8787</source>
         <translation>Asetukset</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Kopioi</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
-        <translation type="unfinished"></translation>
+        <translation>Kopioi &amp;linkki sijainti</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
-        <translation type="unfinished"></translation>
+        <translation>Rea&amp;goi</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Repl&amp;y</source>
-        <translation type="unfinished"></translation>
+        <translation>Vast&amp;aa</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Muokkaa</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Read receip&amp;ts</source>
-        <translation type="unfinished"></translation>
+        <translation>Lue kuitt&amp;eja</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>&amp;Forward</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Lähetä eteenpäin</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>&amp;Mark as read</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Merkitse luetuksi</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>View raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä sisältö raakamuodossa</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>View decrypted raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä salaukseltaan purettu raaka viesti</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Remo&amp;ve message</source>
-        <translation type="unfinished"></translation>
+        <translation>Poist&amp;a viesti</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Save as</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Tallenna nimellä</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Open in external program</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Avaa ulkoisessa sovelluksessa</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Copy link to eve&amp;nt</source>
-        <translation type="unfinished"></translation>
+        <translation>Kopioi linkki tapaht&amp;umaan</translation>
+    </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>&amp;Mene lainattuun viestiin</translation>
     </message>
 </context>
 <context>
@@ -1019,37 +1178,42 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/NewVerificationRequest.qml" line="+11"/>
         <source>Send Verification Request</source>
-        <translation type="unfinished"></translation>
+        <translation>Lähetä vahvistuspyyntö</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Received Verification Request</source>
+        <translation>Otettiin vastaan vahvistuspyyntö</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
-        <translation type="unfinished"></translation>
+        <translation>Voit vahvistaa laitteesi, jotta sallit muiden nähdä, mitkä niistä oikeasti kuuluvat sinulle. Tämä myös mahdollistaa avaimen varmuuskopioinnin toiminnnan automaattisesti. Vahvistetaanko %1 nyt?</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.</source>
-        <translation type="unfinished"></translation>
+        <translation>Varmistaaksesi, ettei kukaan pahantahtoinen käyttäjä voi salakuunnella salattuja keskustelujanne, voit vahvistaa toisen osapuolen.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 has requested to verify their device %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 on pyytänyt vahvistamaan hänen laitteeensa %2.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 using the device %2 has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 käyttää laitetta, jonka %2 on pyytänyt vahvistamaan.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your device (%1) has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>Laitteesi (%1) on pyytänyt vahvistetuksi tulemista.</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -1059,12 +1223,12 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+0"/>
         <source>Deny</source>
-        <translation type="unfinished"></translation>
+        <translation>Kiellä</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Start verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Aloita vahvistus</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -1072,33 +1236,29 @@ Example: https://server.my:8787</source>
         <translation>Hyväksy</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 lähetti salatun viestin</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>%1 vastasi: %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation>%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1121,7 +1281,7 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/PlaceCall.qml" line="+48"/>
         <source>Place a call to %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Soita henkilölle %1?</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -1129,19 +1289,19 @@ Example: https://server.my:8787</source>
         <translation>Mikrofonia ei löydy.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
-        <translation type="unfinished"></translation>
+        <translation>Ääni</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Video</source>
-        <translation type="unfinished"></translation>
+        <translation>Video</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Näyttö</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -1154,49 +1314,65 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/delegates/Placeholder.qml" line="+11"/>
         <source>unimplemented event: </source>
-        <translation type="unfinished"></translation>
+        <translation>toistaseksi toteuttamaton tapahtuma: </translation>
     </message>
 </context>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
-        <translation type="unfinished"></translation>
+        <translation>Luo uniikki profili, joka mahdollistaa kirjautumisen usealle tilille samanaikaisesti ja useamman nheko-instanssin aloittamisen.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>profile</source>
-        <translation type="unfinished"></translation>
+        <translation>profiili</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>profile name</source>
-        <translation type="unfinished"></translation>
+        <translation>profiilin nimi</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Lukukuittaukset</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Eilen, &amp;1</translation>
     </message>
 </context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Käyttäjänimi</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Käyttäjätunnus ei saa olla tyhjä, ja se saa sisältää vain merkkejä a-z, 0-9, ., _, =, - ja /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Salasana</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Please choose a secure password. The exact requirements for password strength may depend on your server.</source>
-        <translation type="unfinished"></translation>
+        <translation>Valitse turvallinen salasana. Tarkat vaatimukset salasanan vahvuudelle voivat riippua palvelimestasi.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -1209,9 +1385,9 @@ Example: https://server.my:8787</source>
         <translation>Kotipalvelin</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
-        <translation type="unfinished"></translation>
+        <translation>Palvelin, joka sallii rekisteröinnin. Koska matrix on hajautettu, sinun pitää ensin löytää palvelin jolle rekisteröityä tai ylläpitää omaasi.</translation>
     </message>
     <message>
         <location line="+35"/>
@@ -1219,27 +1395,17 @@ Example: https://server.my:8787</source>
         <translation>REKISTERÖIDY</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Palvelimen tietojen hakeminen epäonnistui: virheellinen vastaus.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Palvelimen tietojen hakeminen epäonnistui: tuntematon virhe hakiessa .well-known -tiedostoa.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Vaadittuja päätepisteitä ei löydetty. Mahdollisesti ei Matrix-palvelin.</translation>
     </message>
@@ -1254,17 +1420,17 @@ Example: https://server.my:8787</source>
         <translation>Tapahtui tuntematon virhe. Varmista, että kotipalvelimen osoite on pätevä.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Salasana ei ole tarpeeksi pitkä (vähintään 8 merkkiä)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Salasanat eivät täsmää</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Epäkelpo palvelimen nimi</translation>
     </message>
@@ -1272,7 +1438,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Sulje</translation>
     </message>
@@ -1282,10 +1448,28 @@ Example: https://server.my:8787</source>
         <translation>Peruuta muokkaus</translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Tutki julkisia huoneita</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Etsi julkisia huoneita</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation>ei tallennettua versiota</translation>
     </message>
@@ -1293,24 +1477,14 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
-        <translation type="unfinished"></translation>
+        <translation>Uusi tagi</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter the tag you want to use:</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
+        <translation>Kirjoita tagi jota haluat käyttää:</translation>
     </message>
     <message>
         <location line="+7"/>
@@ -1320,55 +1494,78 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+5"/>
         <source>Tag room as:</source>
-        <translation type="unfinished"></translation>
+        <translation>Laita huoneelle tagi:</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Favourite</source>
-        <translation type="unfinished"></translation>
+        <translation>Suosikki</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Low priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Matala tärkeysjärjestys</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Server notice</source>
-        <translation type="unfinished"></translation>
+        <translation>Palvelimen ilmoitus</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Create new tag...</source>
-        <translation type="unfinished"></translation>
+        <translation>Luo uusi tagi...</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Tilapäivitys</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter your status message:</source>
-        <translation type="unfinished"></translation>
+        <translation>Kirjoita tilapäivityksesi:</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Profile settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Profiilin asetukset</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Set status message</source>
-        <translation type="unfinished"></translation>
+        <translation>Aseta tilapäivitys</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation>Kirjaudu ulos</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Sulje</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation>Aloita uusi keskustelu</translation>
     </message>
@@ -1380,7 +1577,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+5"/>
         <source>Create a new room</source>
-        <translation type="unfinished"></translation>
+        <translation>Luo uusi huone</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -1388,7 +1585,7 @@ Example: https://server.my:8787</source>
         <translation>Huoneluettelo</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation>Käyttäjäasetukset</translation>
     </message>
@@ -1396,79 +1593,114 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;1 jäsenet</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%n henkilö huoneessa %1</numerusform>
+            <numerusform>%n henkilöä huonessa %1</numerusform>
         </translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Invite more people</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsu lisää ihmisiä</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Tämä huone ei ole salattu!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Tämä käyttäjä on vahvistettu.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Tätä käyttäjää ei ole vahvistettu, mutta hän käyttää edelleen samaa päävavainta kuin ensimmäisellä tapaamiskerralla.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Tällä käyttäjällä on vahvistamattomia laitteita!</translation>
     </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Huoneen asetukset</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 jäsentä</translation>
     </message>
     <message>
         <location line="+55"/>
         <source>SETTINGS</source>
-        <translation type="unfinished"></translation>
+        <translation>ASETUKSET</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Notifications</source>
-        <translation type="unfinished"></translation>
+        <translation>Ilmoitukset</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Muted</source>
-        <translation type="unfinished"></translation>
+        <translation>Mykistetty</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Mentions only</source>
-        <translation type="unfinished"></translation>
+        <translation>Vain maininnat</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All messages</source>
-        <translation type="unfinished"></translation>
+        <translation>Kaikki viestit</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Huoneeseen pääsy</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
-        <translation type="unfinished"></translation>
+        <translation>Kaikki ja vieraat</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Anyone</source>
-        <translation type="unfinished"></translation>
+        <translation>Kuka tahansa</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Invited users</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsutut käyttäjät</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>Koputtamalla</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Rajoitettu jäsenyyden perusteella muissa huoneissa</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Salaus</translation>
     </message>
@@ -1481,32 +1713,32 @@ Example: https://server.my:8787</source>
         <location line="+1"/>
         <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
                             Please take note that it can&apos;t be disabled afterwards.</source>
-        <translation type="unfinished"></translation>
+        <translation>Salaus on tällä hetkellä kokeellinen ja asiat voivat mennä rikki odottamattomasti.&lt;br&gt;Huomaa että sitä ei voi poistaa jälkikäteen.</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Tarra- ja emojiasetukset</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Muuta</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Muuta mitkä pakkaukset ovat sallittuja, poista pakkauksia tai luo uusia</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>INFO</source>
-        <translation type="unfinished"></translation>
+        <translation>TIETOA</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Internal ID</source>
-        <translation type="unfinished"></translation>
+        <translation>Sisäinen ID</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -1514,12 +1746,12 @@ Example: https://server.my:8787</source>
         <translation>Huoneen versio</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation>Salauksen aktivointi epäonnistui: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Valitse profiilikuva</translation>
     </message>
@@ -1539,8 +1771,8 @@ Example: https://server.my:8787</source>
         <translation>Virhe lukiessa tiedostoa: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation>Kuvan lähetys epäonnistui: %s</translation>
     </message>
@@ -1548,18 +1780,46 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>Kutsua odotetaan.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Esikatsellaan tätä huonetta</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
+        <translation>Esikatselu ei saatavilla</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1568,33 +1828,33 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/ScreenShare.qml" line="+30"/>
         <source>Share desktop with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Jaa työpöytä käyttäjän %1 kanssa?</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Window:</source>
-        <translation type="unfinished"></translation>
+        <translation>Ikkuna:</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Frame rate:</source>
-        <translation type="unfinished"></translation>
+        <translation>Ruudunpäivitys:</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Include your camera picture-in-picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Sisällytä kamerasi kuva kuvassa -tilaan</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Request remote camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Pyydä etäkameraa</translation>
     </message>
     <message>
         <location line="+1"/>
         <location line="+9"/>
         <source>View your callee&apos;s camera like a regular video call</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä puhelun vastaanottajan kamera tavallisen videopuhelun tapaan</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -1604,12 +1864,12 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+20"/>
         <source>Share</source>
-        <translation type="unfinished"></translation>
+        <translation>Jaa</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Preview</source>
-        <translation type="unfinished"></translation>
+        <translation>Esikatsele</translation>
     </message>
     <message>
         <location line="+7"/>
@@ -1617,27 +1877,142 @@ Example: https://server.my:8787</source>
         <translation>Peruuta</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Salattuun tallennustilaan ei saatu yhteyttä</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Kuvapakkausta %1 ei onnistuttu päivittämään</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Vanhaa kuvapakkausta %1 ei onnistuttu poistamaan</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Kuvaa %1 ei onnistuttu avaamaan</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Kuvan lähetys epäonnistui: %s</translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
         <location filename="../qml/StatusIndicator.qml" line="+24"/>
         <source>Failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Epäonnnistui</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Sent</source>
-        <translation type="unfinished"></translation>
+        <translation>Lähetetyt</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Received</source>
-        <translation type="unfinished"></translation>
+        <translation>Vastaanotetut</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Read</source>
-        <translation type="unfinished"></translation>
+        <translation>Lue</translation>
     </message>
 </context>
 <context>
@@ -1645,7 +2020,7 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
         <source>Search</source>
-        <translation type="unfinished">Hae</translation>
+        <translation>Hae</translation>
     </message>
 </context>
 <context>
@@ -1653,12 +2028,12 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/Success.qml" line="+11"/>
         <source>Successful Verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Onnistunut varmistus</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Verification successful! Both sides verified their devices!</source>
-        <translation type="unfinished"></translation>
+        <translation>Varmistus onnistui! Molemmat osapuolet vahvistivat laitteensa!</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -1669,18 +2044,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Viestin muokkaus epäonnistui: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
-        <translation type="unfinished"></translation>
+        <translation>Tapahtuman salaus epäonnistui, lähetys keskeytetään!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Tallenna kuva</translation>
     </message>
@@ -1700,7 +2075,7 @@ Example: https://server.my:8787</source>
         <translation>Tallenna tiedosto</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1709,79 +2084,94 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 avasi huoneen julkiseksi.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 made this room require and invitation to join.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 teki tästä huoneesta liittymiskutsun vaativan.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>Käyttäjän %1 annettiin liittyä tähän huoneeseen koputtamalla.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 salli seuraavien huoneiden jäsenten liittyä automaattisesti tähän huoneeseen: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 teki huoneesta avoimen vieraille.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 has closed the room to guest access.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 on sulkenut huoneen vierailta.</translation>
     </message>
     <message>
         <location line="+23"/>
         <source>%1 made the room history world readable. Events may be now read by non-joined people.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 teki huoneen historian luettavaksi kaikille. Tapahtumia voivat nyt lukea myös huoneeseen liittymättömät ihmiset.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>%1 set the room history visible to members from this point on.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 asetti huoneen historian näkyväksi jäsenille tästä lähtien.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 asetti huoneen historian näkyväksi jäsenille kutsumisesta lähtien.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 asetti huoneen historian näkyväksi jäsenille huoneeseen liittymisen jälkeen.</translation>
     </message>
     <message>
         <location line="+22"/>
         <source>%1 has changed the room&apos;s permissions.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 on muuttanut huoneen lupia.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;1 kutsuttiin.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 muutti avatariaan.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 changed some profile info.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 muutti joitain tietoja profiilistaan.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 liittyi.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 liittyi käyttäjän %2 palvelimen suomalla vahvistuksella.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 hylkäsi kutsunsa.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Revoked the invite to %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Peruttiin kutsu käyttäjälle %1.</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -1791,64 +2181,64 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+2"/>
         <source>Kicked %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Potkittiin %1.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Unbanned %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Poistettiin käyttäjän %1 porttikielto.</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>%1 was banned.</source>
-        <translation type="unfinished"></translation>
+        <translation>Käyttäjälle %1 annettiin porttikielto.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Syy: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 perui koputuksensa.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Sinä liityit tähän huoneeseen.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 vaihtoi avatariaan ja vaihtoi näyttönimekseen %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 vaihtoi näyttönimekseen %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Hylättiin koputus käyttäjältä %1.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 left after having already left!</source>
         <comment>This is a leave event after the user already left and shouldn&apos;t happen apart from state resets</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 lähti vaikka lähti jo aiemmin!</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>%1 knocked.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 koputti.</translation>
     </message>
 </context>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Muokattu</translation>
     </message>
@@ -1856,135 +2246,242 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
-        <translation type="unfinished"></translation>
+        <translation>Ei avointa huonetta</translation>
+    </message>
+    <message>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Esikatselu ei saatavilla</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+7"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 jäsentä</translation>
     </message>
     <message>
         <location line="+33"/>
         <source>join the conversation</source>
-        <translation type="unfinished"></translation>
+        <translation>liity keskusteluun</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>accept invite</source>
-        <translation type="unfinished"></translation>
+        <translation>salli kutsu</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>decline invite</source>
-        <translation type="unfinished"></translation>
+        <translation>peru kutsu</translation>
     </message>
     <message>
         <location line="+27"/>
         <source>Back to room list</source>
+        <translation>Takaisin huonelistaan</translation>
+    </message>
+</context>
+<context>
+    <name>TopBar</name>
+    <message>
+        <location filename="../qml/TopBar.qml" line="+59"/>
+        <source>Back to room list</source>
+        <translation>Takaisin huonelistaan</translation>
+    </message>
+    <message>
+        <location line="-44"/>
+        <source>No room selected</source>
+        <translation>Ei valittua huonetta</translation>
+    </message>
+    <message>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Tämä huone ei ole salattu!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Tämä huone sisältää vain vahvistettuja laitteita.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Tämä huone sisältää varmentamattomia laitteita!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>Room options</source>
+        <translation>Huoneen asetukset</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Invite users</source>
+        <translation>Kutsu käyttäjiä</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Members</source>
+        <translation>Jäsenet</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Leave room</source>
+        <translation>Poistu huoneesta</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Settings</source>
+        <translation>Asetukset</translation>
+    </message>
 </context>
 <context>
-    <name>TimelineViewManager</name>
+    <name>TrayIcon</name>
     <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <location filename="../../src/TrayIcon.cpp" line="+112"/>
+        <source>Show</source>
+        <translation>Näytä</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Quit</source>
+        <translation>Lopeta</translation>
+    </message>
+</context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
-        <source>Back to room list</source>
-        <translation type="unfinished"></translation>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Anna kelvollinen rekisteröitymispoletti.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>UserProfile</name>
+    <message>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
+        <source>Global User Profile</source>
+        <translation>Yleinen käyttäjäprofiili</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Room User Profile</source>
+        <translation>Huoneen käyttäjäprofiili</translation>
+    </message>
+    <message>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Vaihda avataria kaikkialla.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Muuta avataria. Toimii vain tässä huoneessa.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Muuta näyttönimeä kaikkialla.</translation>
     </message>
     <message>
-        <location line="-39"/>
-        <source>No room selected</source>
-        <translation type="unfinished"></translation>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Muuta näyttönimeä. Toimii vain tässä huoneessa.</translation>
     </message>
     <message>
-        <location line="+90"/>
-        <source>Room options</source>
-        <translation>Huoneen asetukset</translation>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Huone: %1</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Invite users</source>
-        <translation>Kutsu käyttäjiä</translation>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>Tämä on huoneelle erityinen profiili. Käyttäjän nimi ja avatar voivat erota niiden kaikkialla käytössä olevista versioista.</translation>
     </message>
     <message>
-        <location line="+5"/>
-        <source>Members</source>
-        <translation>Jäsenet</translation>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Avaa tämän käyttäjän yleinen profiili.</translation>
     </message>
     <message>
-        <location line="+5"/>
-        <source>Leave room</source>
-        <translation>Poistu huoneesta</translation>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation>Vahvista</translation>
     </message>
     <message>
-        <location line="+5"/>
-        <source>Settings</source>
-        <translation>Asetukset</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Aloita yksityinen keskustelu.</translation>
     </message>
-</context>
-<context>
-    <name>TrayIcon</name>
     <message>
-        <location filename="../../src/TrayIcon.cpp" line="+112"/>
-        <source>Show</source>
-        <translation>Näytä</translation>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Potki käyttäjä.</translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Quit</source>
-        <translation>Lopeta</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Anna käyttäjälle porttikielto.</translation>
     </message>
-</context>
-<context>
-    <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
-        <source>Global User Profile</source>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+0"/>
-        <source>Room User Profile</source>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+31"/>
+        <source>Change device name.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation type="unfinished"></translation>
+        <location line="+27"/>
+        <source>Unverify</source>
+        <translation>Peru vahvistus</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
-        <source>Unverify</source>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Valitse profiilikuva</translation>
     </message>
@@ -2007,16 +2504,16 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Oletus</translation>
     </message>
 </context>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Pienennä ilmoitusalueelle</translation>
     </message>
@@ -2026,119 +2523,134 @@ Example: https://server.my:8787</source>
         <translation>Aloita ilmoitusalueella</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Ryhmäsivupalkki</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
-        <translation type="unfinished"></translation>
+        <translation>Pyöreät avatarit</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>profiili: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Oletus</translation>
     </message>
     <message>
         <location line="+31"/>
         <source>CALLS</source>
-        <translation type="unfinished"></translation>
+        <translation>PUHELUT</translation>
     </message>
     <message>
         <location line="+46"/>
         <source>Cross Signing Keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Ristiin allekirjoitetut avaimet</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>REQUEST</source>
-        <translation type="unfinished"></translation>
+        <translation>PYYNTÖ</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>DOWNLOAD</source>
-        <translation type="unfinished"></translation>
+        <translation>LATAA</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
-        <translation type="unfinished"></translation>
+        <translation>Anna sovelluksen pyöriä taustalla asiakasohjelman ikkunan sulkemisen jälkeen.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Start the application in the background without showing the client window.</source>
-        <translation type="unfinished"></translation>
+        <translation>Aloita sovellus taustalla näyttämättä asiakasohjelman ikkunaa.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change the appearance of user avatars in chats.
 OFF - square, ON - Circle.</source>
+        <translation>Muuta käyttäjien avatarien ulkonäköä keskusteluissa.
+POIS PÄÄLTÄ - neliö, PÄÄLLÄ - pyöreä.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä huonelistan vieressä tagit ja ryhmät sisältävä sarake.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Decrypt messages in sidebar</source>
-        <translation type="unfinished"></translation>
+        <translation>Pura viestien salaus sivupalkissa</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Decrypt the messages shown in the sidebar.
 Only affects messages in encrypted chats.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pura sivupalkissa näkyvien viestien salaus
+Vaikuttaa vain salattujen keskustelujen viesteihin.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Privacy Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Yksityisyysnäkymä</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>When the window loses focus, the timeline will
 be blurred.</source>
-        <translation type="unfinished"></translation>
+        <translation>Kun ikkuna ei ole kohdistettuna, tämä aikajana
+sumennetaan.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
-        <translation type="unfinished"></translation>
+        <translation>Yksityisyysnäkymän aikakatkaisu (sekunneissa [0-3600])</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set timeout (in seconds) for how long after window loses
 focus before the screen will be blurred.
 Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds)</source>
-        <translation type="unfinished"></translation>
+        <translation>Aseta aikakatkaisu (sekunneissa) ikkunan kohdistuksen kadottamiselle
+ennen kuin näkymä sumennetaan.
+Aseta nollaan, jotta sumennetaan heti kohdistus kadotetaan. Suurin arvo 1 tunti (3600 sekuntia)</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Show buttons in timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä painikkeet aikajanalla</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show buttons to quickly reply, react or access additional options next to each message.</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä painikkeet vastataksesi nopeasti, reagoidaksesi tai päästäksesi lisätoimintoihin joka viestin vieressä.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Limit width of timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Rajoita aikajanan leveyttä</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised</source>
-        <translation type="unfinished"></translation>
+        <translation>Aseta viestien suurin leveys aikajanalla (pikseleissä). Tämä voi auttaa luettavuutta laajakuvassa, kun Nheko on täyden ruudun tilassa.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2149,22 +2661,25 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds
         <location line="+2"/>
         <source>Show who is typing in a room.
 This will also enable or disable sending typing notifications to others.</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä kuka kirjoittaa huoneessa.
+Tämä myös sallii tai evää kirjoitusilmoitusten lähettämisen muille.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Sort rooms by unreads</source>
-        <translation type="unfinished"></translation>
+        <translation>Lajittele huoneet lukemattomien mukaan</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Display rooms with new messages first.
 If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room.
 If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don&apos;t seem to consider them as important as the other rooms.</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä ensiksi huoneet, joissa on uusia viestejä.
+Jos tämä on poissa päältä, lista huoneista lajitellaan vain huoneen viimeisimmän viestin aikaleiman mukaan.
+Jos tämä on päällä, huoneet, joissa ilmoitukset ovat päällä (pieni ympyrä, jonka sisässä on numero), lajitellaan päällimmäisiksi. Mykistämäsi huoneet lajitellaan aikaleiman mukaan, koska et nähtävästi pidä niitä yhtä tärkeinä kuin muita huoneita.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Lukukuittaukset</translation>
     </message>
@@ -2172,84 +2687,127 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <location line="+2"/>
         <source>Show if your message was read.
 Status is displayed next to timestamps.</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä jos viestisi oli luettu.
+Tila näytetään aikaleimojen vieressä.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
-        <translation type="unfinished"></translation>
+        <translation>Lähetä viestit Markdownina</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Allow using markdown in messages.
 When disabled, all messages are sent as a plain text.</source>
-        <translation type="unfinished"></translation>
+        <translation>Salli Markdownin käyttö viesteissä.
+Kun poissa päältä, kaikki viestit lähetetään tavallisena tekstinä.</translation>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Toista animoidut kuvat vain kun kohdistin on niiden päällä</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Työpöytäilmoitukset</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Notify about received message when the client is not currently focused.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ilmoita vastaanotetusta viestistä kun ohjelma ei ole kohdistettuna.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Alert on notification</source>
-        <translation type="unfinished"></translation>
+        <translation>Hälytä ilmoituksesta</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show an alert when a message is received.
 This usually causes the application icon in the task bar to animate in some fashion.</source>
-        <translation type="unfinished"></translation>
+        <translation>Näytä hälytys kun viesti on vastaanotettu.
+Tämä yleensä saa sovelluksen kuvakkeen liikkumaan jollain tapaa tehtäväpalkissa.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Highlight message on hover</source>
-        <translation type="unfinished"></translation>
+        <translation>Korosta viestiä kun kohdistin on päällä</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the background color of messages when you hover over them.</source>
-        <translation type="unfinished"></translation>
+        <translation>Muuta viestien taustaväriä kun kohdistimesi liikkuu niiden yli.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Large Emoji in timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Iso emoji aikajanalla</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Make font size larger if messages with only a few emojis are displayed.</source>
-        <translation type="unfinished"></translation>
+        <translation>Suurenna fonttikokoa jos näytetään viestit vain muutamalla emojilla.</translation>
+    </message>
+    <message>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Lähetä salatut viestit vain vahvistetuille käyttäjille</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Vaatii käyttäjän olevan vahvistettu, jotta hänelle voi lähettää salattuja viestejä. Tämä parantaa turvallisuutta, mutta tekee päästä-päähän -salauksen hankalammaksi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
+        <translation>Jaa avaimet vahvistettujen käyttäjien ja laitteiden kanssa</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Automaattisesti vastaa avainpyyntöihin, jos ne ovat vahvistettuja, vaikka tuolla laitteella ei tulisi muuten olla pääsyä noihin avaimiin.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Avaimen varmuuskopiointi verkkoon</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Salli avaimen varmuuskopiointi verkkoon</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>Nhekon tekijät eivät suosittele avaimen varmuuskopiointia verkkoon kunnes avaimen symmetrinen varmuuskopiointi verkkoon on saatavilla. Sallitaanko se kuitenkin?</translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>VÄLIMUISTISSA</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>NOT CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>EI VÄLIMUISTISSA</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Mittakerroin</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the scale factor of the whole user interface.</source>
-        <translation type="unfinished"></translation>
+        <translation>Muuta koko käyttöliittymän kokoa.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2274,7 +2832,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+2"/>
         <source>Set the notification sound to play when a call invite arrives</source>
-        <translation type="unfinished"></translation>
+        <translation>Aseta ilmoitusääni soimaan kun kutsu puheluun tulee</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2289,22 +2847,22 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+1"/>
         <source>Camera resolution</source>
-        <translation type="unfinished"></translation>
+        <translation>Kameran resoluutio</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera frame rate</source>
-        <translation type="unfinished"></translation>
+        <translation>Kameran ruudunpäivitys</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Allow fallback call assist server</source>
-        <translation type="unfinished"></translation>
+        <translation>Salli varajärjestelynä toimiva puhelua avustava palvelin</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will use turn.matrix.org as assist when your home server does not offer one.</source>
-        <translation type="unfinished"></translation>
+        <translation>Käyttää apuna palvelinta turn.matrix.org silloin kun kotipalvelimesi ei sellaista tarjoa.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2317,7 +2875,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Laitteen sormenjälki</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Istunnon avaimet</translation>
     </message>
@@ -2337,74 +2895,74 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>SALAUS</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>YLEISET ASETUKSET</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
-        <translation type="unfinished"></translation>
+        <translation>KÄYTTÖLIITTYMÄ</translation>
+    </message>
+    <message>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Soittaa mediaa kuten GIF- ja WEBP-tiedostoja vain kun kursori on niiden kohdalla.</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
-        <translation type="unfinished"></translation>
+        <translation>Kosketusnäyttötila</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will prevent text selection in the timeline to make touch scrolling easier.</source>
-        <translation type="unfinished"></translation>
+        <translation>Estää tekstin valitsemisen aikajanalla, jotta koskettamalla vierittäminen on helpompaa.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Emoji Font Family</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>Emojien fonttiperhe</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Päätason allekirjoittava avain</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your most important key. You don&apos;t need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys.</source>
-        <translation type="unfinished"></translation>
+        <translation>Kaikkein tärkein avaimesi. Sinun ei tarvitse laittaa sitä välimuistiin, koska silloin sen varastaminen on epätodennäköistä ja sitä vaaditaan vain kierrättämään muita allekirjoittavia avaimiasi.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>User signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Käyttäjän allekirjoittava avain</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify other users. If it is cached, verifying a user will verify all their devices.</source>
-        <translation type="unfinished"></translation>
+        <translation>Avain vahvistamaan muita käyttäjiä. Jos se on välimuistissa, käyttäjän varmistaminen varmistaa hänen kaikki laitteensa.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Itsensä allekirjoittava avain</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you.</source>
-        <translation type="unfinished"></translation>
+        <translation>Avain vahvistamaan omat avaimesi. Jos se on välimuistisas, yhden laitteesi vahvistaminen laittaa sen vahvistetuksi kaikille muille laitteillesi ja käyttäjille, jotka ovat vahvistaneet sinut.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Backup key</source>
-        <translation type="unfinished"></translation>
+        <translation>Varmuuskopioavain</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server.</source>
-        <translation type="unfinished"></translation>
+        <translation>Avain purkamaan avainten varmuuskopioita verkossa. Jos se laitetaan välimuistiin, voit sallia avainten varmuuskopioinnin verkossa säilöäksesi salausavaimet, jotka ovat turvallisesti salattuja palvelimella.</translation>
     </message>
     <message>
         <location line="+54"/>
@@ -2417,14 +2975,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Kaikki Tiedostot (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Avaa Istuntoavaintiedosto</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2432,19 +2990,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Virhe</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Tiedoston Salasana</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Anna salasana tiedoston salauksen purkamiseksi:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>Salasana ei voi olla tyhjä</translation>
     </message>
@@ -2459,27 +3017,35 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Tiedosto, johon viedyt istuntoavaimet tallennetaan</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Salattua keskustelua ei löydetty tälle käyttäjälle. Luo salattu yksityiskeskustelu tämän käyttäjän kanssa ja yritä uudestaan.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
         <location filename="../qml/device-verification/Waiting.qml" line="+12"/>
         <source>Waiting for other party…</source>
-        <translation type="unfinished"></translation>
+        <translation>Odotetaan toista osapuolta…</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Waiting for other side to accept the verification request.</source>
-        <translation type="unfinished"></translation>
+        <translation>Odotetaan toista osapuolta hyväksymään vahvistuspyyntö.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to continue the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Odotetaan toista puolta jatkamaan vahvistusta.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to complete the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Odotetaan toista puolta saamaan vahvistus valmiiksi.</translation>
     </message>
     <message>
         <location line="+15"/>
@@ -2497,7 +3063,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+1"/>
         <source>Enjoy your stay!</source>
-        <translation type="unfinished"></translation>
+        <translation>Nauti vierailustasi!</translation>
     </message>
     <message>
         <location line="+23"/>
@@ -2566,7 +3132,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/FallbackAuth.cpp" line="+34"/>
         <source>Open Fallback in Browser</source>
-        <translation type="unfinished"></translation>
+        <translation>Avaa varajärjestely selaimessa</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2581,38 +3147,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+12"/>
         <source>Open the fallback, follow the steps and confirm after completing them.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Liity</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Peruuta</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Huoneen tunnus tai osoite</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Peruuta</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Oletko varma, että haluat poistua?</translation>
+        <translation>Avaa varajärjestely, seuraa ohjeita ja vahvista kun olet saanut ne valmiiksi.</translation>
     </message>
 </context>
 <context>
@@ -2668,32 +3203,6 @@ Median koko: %2
         <translation>Ratkaise reCAPTCHA ja paina varmista-nappia</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Lukukuittaukset</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Sulje</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Tänään %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Eilen %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2707,47 +3216,47 @@ Median koko: %2
         <translation>%1 lähetti äänileikkeen</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Lähetit kuvan</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 lähetti kuvan</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Lähetit tiedoston</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 lähetti tiedoston</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Lähetit videotiedoston</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 lähetti videotiedoston</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Lähetit tarran</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 lähetti tarran</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Lähetit ilmoituksen</translation>
     </message>
@@ -2762,7 +3271,7 @@ Median koko: %2
         <translation>Sinä: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2782,27 +3291,27 @@ Median koko: %2
         <translation>Soitit puhelun</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 soitti puhelun</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Vastasit puheluun</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 vastasi puheluun</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Lopetit puhelun</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 lopetti puhelun</translation>
     </message>
@@ -2810,7 +3319,7 @@ Median koko: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Tuntematon viestityyppi</translation>
     </message>
diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts
index c6d4229963bcf5ecea3f89a40fbfff5e849fb3f8..f57f89845da2386c1944546065c148bf7795e852 100644
--- a/resources/langs/nheko_fr.ts
+++ b/resources/langs/nheko_fr.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Appel en cours…</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Appel vidéo</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Appel vidéo</translation>
     </message>
@@ -91,17 +91,17 @@
     <message>
         <location line="+10"/>
         <source>Accept</source>
-        <translation type="unfinished">Décrocher</translation>
+        <translation>Décrocher</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Unknown microphone: %1</source>
-        <translation>Microphone inconnu&#xa0;&#xa0;: %1</translation>
+        <translation>Microphone inconnu&#xa0;: %1</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Unknown camera: %1</source>
-        <translation>Caméra inconnue&#xa0;&#xa0;: %1</translation>
+        <translation>Caméra inconnue&#xa0;: %1</translation>
     </message>
     <message>
         <location line="+13"/>
@@ -111,37 +111,37 @@
     <message>
         <location line="-28"/>
         <source>No microphone found.</source>
-        <translation>Pas de microphone trouvé.</translation>
+        <translation>Aucun microphone trouvé.</translation>
     </message>
 </context>
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
-        <translation>L&apos;écran complet</translation>
+        <translation>Tout l&apos;écran</translation>
     </message>
 </context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Échec lors de l&apos;invitation de %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
-        <translation>%1 a été invité(e)</translation>
+        <translation>Utilisateur %1 invité(e)</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>La migration du cache vers la version actuelle a échoué. Cela peut arriver pour différentes raisons. Signalez le problème et essayez d&apos;utiliser une ancienne version en attendant. Vous pouvez également supprimer le cache manuellement.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Confirmez la participation</translation>
     </message>
@@ -151,23 +151,23 @@
         <translation>Voulez-vous vraiment rejoindre %1 ?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Salon %1 créé.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Confirmer l&apos;invitation</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Voulez-vous vraiment inviter %1 (%2)&#x202f;?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Échec de l&apos;invitation de %1 dans %2&#xa0;: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Voulez-vous vraiment expulser %1 (%2)&#x202f;?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>L&apos;utilisateur %1 a été expulsé.</translation>
     </message>
@@ -197,9 +197,9 @@
         <translation>Voulez-vous vraiment bannir %1 (%2)&#x202f;?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
-        <translation>L&apos;utilisateur %1 n&apos;a pas pu être banni de %2 : %3</translation>
+        <translation>Échec du bannissement de %1 de %2 : %3</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -217,7 +217,7 @@
         <translation>Voulez-vous vraiment annuler le bannissement de %1 (%2)&#x202f;?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Échec de l&apos;annulation du bannissement de %1 dans %2&#xa0;: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>%1 n&apos;est plus banni(e)</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
-        <translation>Voulez-vous vraimer commencer une discussion privée avec %1 ?</translation>
+        <translation>Voulez-vous vraiment commencer une discussion privée avec %1 ?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Échec de la migration du cache&#x202f;!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>Le cache sur votre disque est plus récent que cette version de Nheko ne supporte. Veuillez mettre à jour ou supprimer votre cache.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Échec de la restauration du compte OLM. Veuillez vous reconnecter.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Échec de la restauration des données sauvegardées. Veuillez vous reconnecter.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Échec de la configuration des clés de chiffrement. Réponse du serveur&#xa0;: %1 %2. Veuillez réessayer plus tard.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
-        <translation>Veuillez vous reconnecter&#xa0;: %1</translation>
+        <translation>Veuillez re-tenter vous reconnecter&#xa0;: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Impossible de rejoindre le salon&#xa0;: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Vous avez rejoint le salon</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Impossible de supprimer l&apos;invitation&#x202f;: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Échec de la création du salon&#xa0;: %1</translation>
     </message>
@@ -293,9 +295,9 @@
         <translation>Impossible de quitter le salon&#xa0;: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
-        <translation>Échec de l&apos;expulsion de %1 depuis %2&#x202f;&#x202f;: %3</translation>
+        <translation>Échec de l&apos;expulsion de %1 de %2&#x202f;&#x202f;: %3</translation>
     </message>
 </context>
 <context>
@@ -303,7 +305,7 @@
     <message>
         <location filename="../qml/CommunitiesList.qml" line="+44"/>
         <source>Hide rooms with this tag or from this space by default.</source>
-        <translation type="unfinished"></translation>
+        <translation>Cacher par défaut les salons avec cette étiquette ou de cet espace.</translation>
     </message>
 </context>
 <context>
@@ -311,63 +313,63 @@
     <message>
         <location filename="../../src/timeline/CommunitiesModel.cpp" line="+37"/>
         <source>All rooms</source>
-        <translation type="unfinished">Tous les salons</translation>
+        <translation>Tous les salons</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Shows all rooms without filtering.</source>
-        <translation type="unfinished"></translation>
+        <translation>Montre tous les salons sans filtrer.</translation>
     </message>
     <message>
         <location line="+30"/>
         <source>Favourites</source>
-        <translation type="unfinished"></translation>
+        <translation>Favoris</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms you have favourited.</source>
-        <translation type="unfinished"></translation>
+        <translation>Vos salons favoris.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Low Priority</source>
-        <translation type="unfinished">Basse priorité</translation>
+        <translation>Priorité basse</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms with low priority.</source>
-        <translation type="unfinished"></translation>
+        <translation>Salons à priorité basse.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Server Notices</source>
-        <translation type="unfinished">Notifications du serveur</translation>
+        <translation>Notifications du serveur</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Messages from your server or administrator.</source>
-        <translation type="unfinished"></translation>
+        <translation>Messages de votre serveur ou administrateur.</translation>
     </message>
 </context>
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Déchiffrer les secrets</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Enter your recovery key or passphrase to decrypt your secrets:</source>
-        <translation>Entrez votre clé de récupération ou phrase de passe pour déchiffrer vos secrets&#xa0;&#xa0;:</translation>
+        <translation>Entrez votre clé de récupération ou phrase de passe pour déchiffrer vos secrets&#xa0;:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
-        <translation>Entrez votre clé de récupération ou votre phrase de passe nommée %1 pour déchiffrer vos secrets&#xa0;&#xa0;:</translation>
+        <translation>Entrez votre clé de récupération ou votre phrase de passe nommée %1 pour déchiffrer vos secrets&#xa0;:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Échec du déchiffrement</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Chercher</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Personnes</translation>
     </message>
@@ -481,85 +483,83 @@
     <message>
         <location line="+10"/>
         <source>Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
-        <translation>Veuillez vérifier les émoji suivantes. Vous devriez voir les mêmes émoji des deux côtés. Si celles-ci diffèrent, veuillez choisir «&#x202f;Elles sont différentes&#x202f;!&#x202f;» pour annuler la vérification&#x202f;!</translation>
+        <translation>Veuillez vérifier les émoji suivants. Vous devriez voir les mêmes émoji des deux côtés. S&apos;ils diffèrent, veuillez choisir « Ils sont différents&#x202f;!&#x202f;» pour annuler la vérification&#x202f;!</translation>
     </message>
     <message>
         <location line="+376"/>
         <source>They do not match!</source>
-        <translation>Elles sont différentes&#x202f;!</translation>
+        <translation>Ils sont différents&#x202f;!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>They match!</source>
-        <translation>Elles sont identiques&#x202f;!</translation>
+        <translation>Ils sont identiques&#x202f;!</translation>
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Ce message n&apos;est pas chiffré&#x202f;!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Il n&apos;y a pas de clé pour déverrouiller ce message. Nous avons demandé la clé automatiquement, mais vous pouvez tenter de la demander à nouveau si vous êtes impatient.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Ce message n&apos;a pas pu être déchiffré, car nous n&apos;avons une clef que pour des messages plus récents. Vous pouvez demander l&apos;accès à ce message.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation type="unfinished"></translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Une erreur interne s&apos;est produite durant la lecture de la clef de déchiffrement depuis la base de données.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation type="unfinished"></translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Une erreur s&apos;est produite durant le déchiffrement de ce message.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>-- Évènement chiffré (pas de clé trouvée pour le déchiffrement) --</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Le message n&apos;a pas pu être traité.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation>-- Événement chiffré (clé invalide pour cet index) --</translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>La clef de chiffrement a été réutilisée ! Quelqu&apos;un essaye peut-être d&apos;insérer de faux messages dans ce chat !</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>-- Échec du déchiffrement (échec de la récupération des clés megolm depuis la base de données) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Erreur de déchiffrement inconnue</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Erreur de déchiffrement (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Demander la clef</translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>-- Évènement chiffré (type d&apos;évènement inconnu) --</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Ce message n&apos;est pas chiffré&#x202f;!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Attaque par rejeu (replay attack)&#x202f;! Cet index de message a été réutilisé&#x202f;! --</translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Chiffré par un appareil vérifié</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Message d&apos;un appareil non vérifié&#x202f; --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Chiffré par un appareil non vérifié, mais vous avez déjà fait confiance à ce contact.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Chiffré par un appareil non vérifié, ou la clef provient d&apos;une source non sûre comme la sauvegarde des clefs.</translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Délai dépassé pour la vérification de l&apos;appareil.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Le correspondant a annulé la vérification.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Fermer</translation>
     </message>
@@ -601,7 +610,82 @@
     <message>
         <location filename="../qml/ForwardCompleter.qml" line="+44"/>
         <source>Forward Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Transférer le message</translation>
+    </message>
+</context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Modification du paquet d&apos;images</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Ajouter des images</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Autocollants (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>Clef d&apos;état</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Nom de paquet</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Attribution</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Utiliser en tant qu&apos;émoji</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Utiliser en tant qu&apos;autocollant</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Raccourci</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Corps</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Retirer du paquet</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Retirer</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Annuler</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Sauvegarder</translation>
     </message>
 </context>
 <context>
@@ -609,43 +693,58 @@
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Paramètres des paquets d&apos;images</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Créer un paquet de compte</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Nouveau paquet de salle</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Paquet privé</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Paquet de cette salle</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Paquet activé partout</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Activer partout</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Permet d&apos;utiliser ce paquet dans tous les salons</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Modifier</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished">Fermer</translation>
+        <translation>Fermer</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Sélectionnez un fichier</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation>Tous les types de fichiers (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Échec de l&apos;envoi du média. Veuillez réessayer.</translation>
     </message>
@@ -663,35 +762,61 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Inviter des utilisateurs dans %1</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>User ID to invite</source>
-        <translation type="unfinished">Identifiant d&apos;utilisateur à inviter</translation>
+        <translation>Identifiant de l&apos;utilisateur à inviter</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@jean:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>Ajouter</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Inviter</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuler</translation>
+        <translation>Annuler</translation>
+    </message>
+</context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Identifiant ou alias du salon</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Quitter le salon</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Êtes-vous sûr·e de vouloir quitter ?</translation>
     </message>
 </context>
 <context>
@@ -704,7 +829,7 @@
     <message>
         <location line="+2"/>
         <source>e.g @joe:matrix.org</source>
-        <translation>ex : @joe:matrix.org</translation>
+        <translation>p. ex : @jean:matrix.org</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -735,7 +860,7 @@ Si Nheko n&apos;arrive pas à trouver votre serveur, il vous proposera de l&apos
     <message>
         <location line="+2"/>
         <source>A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.</source>
-        <translation>Un nom pour cet appareil, qui sera montré aux autres utilisateurs lorsque ceux-ci le vérifieront. Si aucun n&apos;est fourni, un nom par défaut est utilisé.</translation>
+        <translation>Un nom pour cet appareil, qui sera montré aux autres utilisateurs lorsque ceux-ci vérifient vos appareils. Si aucun n&apos;est fourni, un nom par défaut est utilisé.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -745,14 +870,14 @@ Si Nheko n&apos;arrive pas à trouver votre serveur, il vous proposera de l&apos
     <message>
         <location line="+1"/>
         <source>server.my:8787</source>
-        <translation>mon.serveur.fr:8787</translation>
+        <translation>monserveur.example.com:8787</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The address that can be used to contact you homeservers client API.
 Example: https://server.my:8787</source>
         <translation>L&apos;adresse qui peut être utilisée pour joindre l&apos;API client de votre serveur.
-Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
+Exemple&#xa0;: https&#x202f;://monserveur.example.com:8787</translation>
     </message>
     <message>
         <location line="+19"/>
@@ -760,27 +885,27 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>CONNEXION</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
-        <translation>Vous avez entré un identifiant Matrix invalide (exemple correct&#x202f;: @moi&#x202f;:mon.serveur.fr)</translation>
+        <translation>Vous avez entré un identifiant Matrix invalide  exemple correct&#x202f;: @moi:monserveur.example.com)</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation>Échec de la découverte automatique. Réponse mal formatée reçue.</translation>
+        <translation>Échec de la découverte automatique. Réponse mal formée reçue.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Échec de la découverte automatique. Erreur inconnue lors de la demande de .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation>Les chemins requis n&apos;ont pas été trouvés. Possible qu&apos;il ne s&apos;agisse pas d&apos;un serveur Matrix.</translation>
+        <translation>Les endpoints requis n&apos;ont pas été trouvés. Ce n&apos;est peut-être pas un serveur Matrix.</translation>
     </message>
     <message>
         <location line="+6"/>
@@ -788,30 +913,48 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Réponse mal formée reçue. Vérifiez que le nom de domaine du serveur est valide.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Une erreur inconnue est survenue. Vérifiez que le nom de domaine du serveur est valide.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>CONNEXION SSO</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Mot de passe vide</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>Échec de la connexion SSO</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>retiré</translation>
@@ -822,7 +965,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Chiffrement activé</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>nom du salon changé en&#xa0;: %1</translation>
     </message>
@@ -834,7 +977,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+12"/>
         <source>topic changed to: %1</source>
-        <translation>sujet changé pour&#xa0;: %1</translation>
+        <translation>sujet changé en&#xa0;: %1</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -844,7 +987,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+12"/>
         <source>%1 changed the room avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 a changé l&apos;avatar du salon</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -881,6 +1024,11 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <source>Negotiating call...</source>
         <translation>Négociation de l&apos;appel…</translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Les laisser entrer</translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -905,9 +1053,9 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Écrivez un message…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>Autocollants</translation>
     </message>
     <message>
         <location line="+24"/>
@@ -922,13 +1070,13 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+11"/>
         <source>You don&apos;t have permission to send messages in this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Vous n&apos;avez pas l&apos;autorisation d&apos;envoyer des messages dans ce salon</translation>
     </message>
 </context>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Modifier</translation>
     </message>
@@ -948,74 +1096,81 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Options</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Copier</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
-        <translation type="unfinished"></translation>
+        <translation>Copier l&apos;adresse du &amp;lien</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
-        <translation type="unfinished"></translation>
+        <translation>Ré&amp;agir</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Repl&amp;y</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Y répondre</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Editer</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Read receip&amp;ts</source>
-        <translation type="unfinished"></translation>
+        <translation>Accusés de lec&amp;ture</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>&amp;Forward</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Faire suivre</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>&amp;Mark as read</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Marquer comme lu</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>View raw message</source>
-        <translation type="unfinished">Voir le message brut</translation>
+        <translation>Voir le message brut</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>View decrypted raw message</source>
-        <translation type="unfinished">Voir le message déchiffré brut</translation>
+        <translation>Voir le message déchiffré brut</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Remo&amp;ve message</source>
-        <translation type="unfinished"></translation>
+        <translation>Enle&amp;ver le message</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Save as</source>
-        <translation type="unfinished"></translation>
+        <translation>Enregistrer &amp;sous</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Open in external program</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Ouvrir dans un programme externe</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Copy link to eve&amp;nt</source>
-        <translation type="unfinished"></translation>
+        <translation>Copier le lien vers l&apos;évène&amp;nement</translation>
+    </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>Aller au messa&amp;ge cité</translation>
     </message>
 </context>
 <context>
@@ -1031,14 +1186,19 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Demande de vérification reçue</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
-        <translation>Pour permettre aux autres utilisateurs de vérifier quels appareils de votre compte sont sous votre contrôle, vous pouvez vérifier ceux-ci. Cela permet également à ces appareils de sauvegarder vos clés de chiffrement automatiquement. Vérifier %1 maintenant&#x202f;?</translation>
+        <translation>Pour permettre aux autres utilisateurs de vérifier quels appareils de votre compte sont réellement les vôtres, vous pouvez les vérifier. Cela permet également à la sauvegarde des clés de fonctionner automatiquement. Vérifier %1 maintenant&#x202f;?</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.</source>
-        <translation>Pour vous assurer que personne n&apos;intercepte vos communications chiffrées, vous pouvez vérifier le correspondant.</translation>
+        <translation>Pour vous assurer que personne ne puisse intercepter vos communications chiffrées, vous pouvez vérifier le correspondant.</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -1076,33 +1236,29 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Accepter</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 a envoyé un message chiffré</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation>* %1 %2</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>%1 a répondu : %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation>%1&#xa0;&#xa0;: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1112,7 +1268,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+6"/>
         <source>%1 replied to a message</source>
-        <translation>%1 a répondu a un message</translation>
+        <translation>%1 a répondu à un message</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -1133,7 +1289,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Pas de microphone trouvé.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Vocal</translation>
     </message>
@@ -1164,9 +1320,9 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
-        <translation>Créer un profil unique, vous permettant de vous connecter simultanément à plusieurs comptes et à lancer plusieurs instances de nheko.</translation>
+        <translation>Créer un profil unique, vous permettant de vous connecter simultanément à plusieurs comptes et de lancer plusieurs instances de nheko.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -1179,21 +1335,37 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>nom du profil</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Accusés de lecture</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Hier, %1</translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Nom d&apos;utilisateur</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
-        <translation>Le nom d&apos;utilisateur ne doit pas être vide, et ne peut contenir que les caractères a à z, 0 à 9, et «&#x202f;. _ = - /&#x202f;».</translation>
+        <translation>Le nom d&apos;utilisateur ne doit pas être vide, et ne peut contenir que les caractères a-z, 0-9, ., _, =, -, et /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Mot de passe</translation>
     </message>
@@ -1213,7 +1385,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Serveur</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Un serveur qui autorise les créations de compte. Matrix étant décentralisé, vous devez tout d&apos;abord trouver un serveur sur lequel vous pouvez vous inscrire, ou bien héberger le vôtre.</translation>
     </message>
@@ -1223,52 +1395,42 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>S&apos;ENREGISTRER</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Pas de méthode d&apos;inscription supportée&#xa0;!</translation>
+        <location line="+169"/>
+        <source>Autodiscovery failed. Received malformed response.</source>
+        <translation>Échec de la découverte automatique. Réponse mal formée reçue.</translation>
     </message>
     <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>Un ou plusieurs champs ont des entrées invalides. Veuillez les corriger et réessayer.</translation>
+        <location line="+5"/>
+        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
+        <translation>Échec de la découverte automatique. Erreur inconnue lors de la demande de .well-known.</translation>
     </message>
     <message>
-        <location line="+23"/>
-        <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished">Échec de la découverte automatique. Réponse mal formatée reçue.</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished">Échec de la découverte automatique. Erreur inconnue lors de la demande de .well-known.</translation>
-    </message>
-    <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished">Les chemins requis n&apos;ont pas été trouvés. Possible qu&apos;il ne s&apos;agisse pas d&apos;un serveur Matrix.</translation>
+        <translation>Les endpoints requis n&apos;ont pas été trouvés. Ce n&apos;est peut-être pas un serveur Matrix.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished">Réponse mal formée reçue. Vérifiez que le nom de domaine du serveur est valide.</translation>
+        <translation>Réponse mal formée reçue. Vérifiez que le nom de domaine du serveur est valide.</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished">Une erreur inconnue est survenue. Vérifiez que le nom de domaine du serveur est valide.</translation>
+        <translation>Une erreur inconnue est survenue. Vérifiez que le nom de domaine du serveur est valide.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Le mot de passe n&apos;est pas assez long (8 caractères minimum)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Les mots de passe ne sont pas identiques</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Le nom du serveur est invalide</translation>
     </message>
@@ -1276,7 +1438,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Fermer</translation>
     </message>
@@ -1286,10 +1448,28 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Abandonner la modification</translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Explorer les salons publics</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Rechercher des salons publics</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation>pas de version enregistrée</translation>
     </message>
@@ -1297,137 +1477,170 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
-        <translation type="unfinished"></translation>
+        <translation>Nouvelle étiquette</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter the tag you want to use:</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
+        <translation>Entrez l&apos;étiquette que vous voulez utiliser :</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
-        <translation type="unfinished">Quitter le salon</translation>
+        <translation>Quitter le salon</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Tag room as:</source>
-        <translation type="unfinished">Étiqueter le salon comme&#xa0;:</translation>
+        <translation>Étiqueter le salon comme&#xa0;:</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Favourite</source>
-        <translation type="unfinished">Favori</translation>
+        <translation>Favori</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Low priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Priorité basse</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Server notice</source>
-        <translation type="unfinished"></translation>
+        <translation>Notification du serveur</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Create new tag...</source>
-        <translation type="unfinished"></translation>
+        <translation>Créer une nouvelle étiquette…</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Message de statut</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter your status message:</source>
-        <translation type="unfinished"></translation>
+        <translation>Entrez votre message de statut :</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Profile settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Paramètres de profil</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Set status message</source>
-        <translation type="unfinished"></translation>
+        <translation>Changer le message de statut</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
-        <translation type="unfinished">Se déconnecter</translation>
+        <translation>Déconnexion</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Fermer</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
-        <translation type="unfinished">Commencer une discussion</translation>
+        <translation>Commencer une nouvelle discussion</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Join a room</source>
-        <translation type="unfinished">Rejoindre un salon</translation>
+        <translation>Rejoindre un salon</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Create a new room</source>
-        <translation type="unfinished"></translation>
+        <translation>Créer un nouveau salon</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Room directory</source>
-        <translation type="unfinished">Annuaire des salons</translation>
+        <translation>Annuaire des salons</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
-        <translation type="unfinished">Paramètres utilisateur</translation>
+        <translation>Paramètres utilisateur</translation>
     </message>
 </context>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Membres de %1</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%n personne dans %1</numerusform>
+            <numerusform>%n personnes dans %1</numerusform>
         </translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Invite more people</source>
-        <translation type="unfinished"></translation>
+        <translation>Inviter plus de personnes</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ce salon n&apos;est pas chiffré !</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Cet utilisateur est vérifié.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Cet utilisateur n&apos;est pas vérifié, mais utilise toujours la même clef maîtresse que la première fois que vous vous êtes rencontrés.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Cet utilisateur a des appareils non vérifiés !</translation>
     </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation>Configuration du salon</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation>%1 membre(s)</translation>
     </message>
@@ -1457,7 +1670,12 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Tous les messages</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Accès au salon</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation>Tous le monde et les invités</translation>
     </message>
@@ -1472,7 +1690,17 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Utilisateurs invités</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>En toquant</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Restreint par l&apos;appartenance à d&apos;autre salons</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Chiffrement</translation>
     </message>
@@ -1485,22 +1713,22 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <location line="+1"/>
         <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
                             Please take note that it can&apos;t be disabled afterwards.</source>
-        <translation>Le chiffrement actuellement expérimental et des comportements inattendus peuvent être rencontrés. &lt;br&gt;Veuillez noter qu&apos;il n&apos;est pas possible de le désactiver par la suite.</translation>
+        <translation>Le chiffrement est actuellement expérimental et des comportements inattendus peuvent apparaître.&lt;br&gt;Veuillez noter qu&apos;il n&apos;est pas possible de le désactiver par la suite.</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Paramètres des autocollants &amp; emotes</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Modifier</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Modifier quels paquets sont activés, retirer des paquets ou bien en créer de nouveaux</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -1518,12 +1746,12 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Version du salon</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
-        <translation>Échec de l&apos;activation du chiffrement&#xa0;&#xa0;: %1</translation>
+        <translation>Échec de l&apos;activation du chiffrement&#xa0;: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Sélectionner un avatar</translation>
     </message>
@@ -1535,35 +1763,63 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+12"/>
         <source>The selected file is not an image</source>
-        <translation type="unfinished">Le fichier sélectionné n&apos;est pas une image</translation>
+        <translation>Le fichier sélectionné n&apos;est pas une image</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Error while reading file: %1</source>
-        <translation type="unfinished">Erreur lors de la lecture du fichier&#xa0;&#xa0;: %1</translation>
+        <translation>Erreur lors de la lecture du fichier&#xa0;: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
-        <translation type="unfinished">Échec de l&apos;envoi de l&apos;image&#xa0;&#xa0;: %s</translation>
+        <translation>Échec de l&apos;envoi de l&apos;image&#xa0;: %s</translation>
     </message>
 </context>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>Invitation en attente.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Prévisualisation du salon</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
+        <translation>Aucune prévisualisation disponible</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1577,12 +1833,12 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+11"/>
         <source>Window:</source>
-        <translation>Fenêtre&#x202f;&#x202f;:</translation>
+        <translation>Fenêtre&#x202f;:</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Frame rate:</source>
-        <translation>Fréquence d&apos;images&#x202f;&#x202f;:</translation>
+        <translation>Fréquence d&apos;images&#x202f;:</translation>
     </message>
     <message>
         <location line="+19"/>
@@ -1618,7 +1874,122 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuler</translation>
+        <translation>Annuler</translation>
+    </message>
+</context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Échec de la connexion au stockage des secrets</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Échec de la mise à jour du paquet d&apos;images : %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Échec de l&apos;effacement de l&apos;ancien paquet d&apos;images : %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Échec de l&apos;ouverture de l&apos;image : %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Échec de l&apos;envoi de l&apos;image : %1</translation>
     </message>
 </context>
 <context>
@@ -1649,7 +2020,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
         <source>Search</source>
-        <translation type="unfinished">Chercher</translation>
+        <translation>Rechercher</translation>
     </message>
 </context>
 <context>
@@ -1662,7 +2033,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+12"/>
         <source>Verification successful! Both sides verified their devices!</source>
-        <translation>Vérification réussie&#x202f;! Les deux côtés ont vérifié leur appareil&#x202f;!</translation>
+        <translation>Vérification réussie&#x202f;!  Les deux côtés ont vérifié leur appareil&#x202f;!</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -1673,18 +2044,18 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Échec de la suppression du message&#xa0;: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation>Échec du chiffrement de l&apos;évènement, envoi abandonné&#x202f;!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Enregistrer l&apos;image</translation>
     </message>
@@ -1704,7 +2075,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Enregistrer le fichier</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1713,9 +2084,9 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
-        <translation>%1 a rendu le salon ouvert au public.</translation>
+        <translation>%1 a ouvert le salon au public.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -1723,7 +2094,17 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>%1 a rendu le rendu le salon joignable uniquement sur invitation.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 a permis de rejoindre ce salon en toquant.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 a permis aux membres des salons suivants de rejoindre automatiquement ce salon : %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 a rendu le salon ouvert aux invités.</translation>
     </message>
@@ -1743,14 +2124,14 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>%1 a rendu l&apos;historique du salon visible aux membre à partir de cet instant.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 a rendu l&apos;historique visible aux membres à partir de leur invitation.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
-        <translation>%1 a rendu l&apos;historique du salon visible à partir de l&apos;instant où un membre le rejoint.</translation>
+        <translation>%1 a rendu l&apos;historique du salon visible aux membres à partir de l&apos;instant où ils le rejoignent.</translation>
     </message>
     <message>
         <location line="+22"/>
@@ -1758,27 +2139,32 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>%1 a changé les permissions du salon.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 a été invité(e).</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 a changé son avatar.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 changed some profile info.</source>
-        <translation>%1 a changé ses informations de profil.</translation>
+        <translation>%1 a changé des informations de profil.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 a rejoint le salon.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 a rejoint via une autorisation de la part du serveur de %2.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 a rejeté son invitation.</translation>
     </message>
@@ -1808,34 +2194,34 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>%1 a été banni.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Raison : %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
-        <translation>%1 ne frappe plus au salon.</translation>
+        <translation>%1 a arrêté de toquer.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Vous avez rejoint ce salon.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 a changé son avatar et changé son surnom en %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 a changé son surnom en %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
-        <translation>%1 a été rejeté après avoir frappé au salon.</translation>
+        <translation>%1 a été rejeté après avoir toqué.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -1846,13 +2232,13 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+10"/>
         <source>%1 knocked.</source>
-        <translation>%1 a frappé au salon.</translation>
+        <translation>%1 a toqué.</translation>
     </message>
 </context>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Modifié</translation>
     </message>
@@ -1860,58 +2246,75 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Aucun salon ouvert</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Aucune prévisualisation disponible</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished">%1 membre(s)</translation>
+        <translation>%1 membre(s)</translation>
     </message>
     <message>
         <location line="+33"/>
         <source>join the conversation</source>
-        <translation type="unfinished"></translation>
+        <translation>rejoindre la conversation</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>accept invite</source>
-        <translation type="unfinished"></translation>
+        <translation>accepter l&apos;invitation</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>decline invite</source>
-        <translation type="unfinished"></translation>
+        <translation>décliner l&apos;invitation</translation>
     </message>
     <message>
         <location line="+27"/>
         <source>Back to room list</source>
-        <translation type="unfinished">Revenir à la liste des salons</translation>
-    </message>
-</context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation>Pas de discussion privée et chiffrée trouvée avec cet utilisateur. Créez-en une et réessayez.</translation>
+        <translation>Revenir à la liste des salons</translation>
     </message>
 </context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation>Revenir à la liste des salons</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation>Pas de salon sélectionné</translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ce salon n&apos;est pas chiffré !</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Ce salon ne contient que des appareils vérifiés.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Ce salon contient des appareils non vérifiés !</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation>Options du salon</translation>
     </message>
@@ -1949,10 +2352,35 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Quitter</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Veuillez entrer un jeton d&apos;enregistrement valide.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation>Profil général de l&apos;utilisateur</translation>
     </message>
@@ -1962,35 +2390,100 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Profil utilisateur spécifique au salon</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Changer l&apos;image de profil partout.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Changer l&apos;image de profil. Ne s&apos;appliquera qu&apos;à ce salon.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Changer de surnom partout.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Changer de surnom. Ne s&apos;appliquera qu&apos;à ce salon.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Salon : %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>Ceci est un profil spécifique à un salon. Le surnom et l&apos;image de profil peuvent être différents de leurs versions globales.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Ouvrir le profil global de cet utilisateur.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
         <translation>Vérifier</translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
-        <translation>Bannir l&apos;utilisateur</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Démarrer une discussion privée.</translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation>Créer une nouvelle discussion privée</translation>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Expulser l&apos;utilisateur.</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
-        <translation>Expulser l&apos;utilisateur</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Bannir l&apos;utilisateur.</translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation>Dé-vérifier</translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
-        <translation>Sélectionner un avatar</translation>
+        <translation>Sélectionnez un avatar</translation>
     </message>
     <message>
         <location line="+0"/>
@@ -2011,8 +2504,8 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation>Défaut</translation>
     </message>
@@ -2020,9 +2513,9 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
-        <translation>Réduire à la barre des tâches</translation>
+        <translation>Réduire dans la barre des tâches</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -2030,22 +2523,22 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>Démarrer dans la barre des tâches</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Barre latérale des groupes</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Avatars circulaires</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation>profil&#x202f;: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation>Défaut</translation>
     </message>
@@ -2057,7 +2550,7 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
     <message>
         <location line="+46"/>
         <source>Cross Signing Keys</source>
-        <translation type="unfinished">Clés d&apos;auto-vérification (Cross-Signing)</translation>
+        <translation>Clés de signature croisée (Cross-Signing)</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2070,14 +2563,14 @@ Exemple&#xa0;: https&#x202f;://monserveur.example.com&#x202f;:8787</translation>
         <translation>TÉLÉCHARGER</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
-        <translation>Conserver l&apos;application en arrière plan après la fermeture de la fenêtre du client.</translation>
+        <translation>Conserver l&apos;application en arrière-plan après avoir fermé la fenêtre du client.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Start the application in the background without showing the client window.</source>
-        <translation>Démarrer l&apos;application en arrière plan sans montrer la fenêtre du client.</translation>
+        <translation>Démarrer l&apos;application en arrière-plan sans montrer la fenêtre du client.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2086,10 +2579,20 @@ OFF - square, ON - Circle.</source>
         <translation>Change l&apos;apparence des avatars des utilisateurs dans les discussions.
 OFF – carré, ON – cercle.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
-        <translation>Affiche une colonne contenant les groupes et tags à côté de la liste des salons.</translation>
+        <translation>Affiche une colonne contenant les groupes et étiquettes à côté de la liste des salons.</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2116,7 +2619,7 @@ be blurred.</source>
 sera floutée.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation>Attente pour l&apos;activation de la protection anti-indiscrétion (en secondes, 0 à 3600)</translation>
     </message>
@@ -2128,7 +2631,7 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds
         <translation>Temps d&apos;attente (en secondes) avant le floutage des
 conversations lorsque la fenêtre n&apos;est plus active.
 Régler à 0 pour flouter immédiatement lorsque la fenêtre n&apos;est plus au premier plan.
-Valeur maximale de une heure (3600 secondes).</translation>
+Valeur maximale d&apos;une heure (3600 secondes).</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -2138,12 +2641,12 @@ Valeur maximale de une heure (3600 secondes).</translation>
     <message>
         <location line="+2"/>
         <source>Show buttons to quickly reply, react or access additional options next to each message.</source>
-        <translation>Montre les boutons de réponse, réaction ou options additionnelles près de chaque message.</translation>
+        <translation>Montre les boutons de réponse, réaction et options additionnelles près de chaque message.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Limit width of timeline</source>
-        <translation>Limiter la largeur de l&apos;historique</translation>
+        <translation>Limiter la largeur de la discussion</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2173,12 +2676,12 @@ Ceci activera ou désactivera également l&apos;envoi de notifications similaire
 If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room.
 If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don&apos;t seem to consider them as important as the other rooms.</source>
         <translation>Montre les salons qui contiennent de nouveaux messages en premier.
-Si non activé, la liste des salons sera uniquement trié en fonction de la date du dernier message.
+Si non activé, la liste des salons sera uniquement triée en fonction de la date du dernier message.
 Si activé, les salons qui ont des notifications actives (le petit cercle avec un chiffre dedans) seront affichés en premier.
-Les salons que vous avez rendu silencieux seront toujours triés par date du dernier message, car ceux-ci sont considérés comme moins importants.</translation>
+Les salons que vous avez rendu silencieux seront toujours triés par date du dernier message, puisqu&apos;ils sont considérés comme moins importants.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Accusés de lecture</translation>
     </message>
@@ -2190,7 +2693,7 @@ Status is displayed next to timestamps.</source>
 Le statut est montré près de la date des messages.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Composer les messages au format Markdown</translation>
     </message>
@@ -2203,13 +2706,18 @@ Lorsque désactivé, tous les messages sont envoyés en texte brut.</translation
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Ne jouer les images animées que quand survolées</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
-        <translation>Notifier sur le bureau</translation>
+        <translation>Notifications sur le bureau</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Notify about received message when the client is not currently focused.</source>
-        <translation>Notifie des messages reçus lorsque la fenêtre du client n&apos;est pas focalisée.</translation>
+        <translation>Notifie des messages reçus lorsque la fenêtre du client n&apos;est pas active.</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2236,20 +2744,55 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
     <message>
         <location line="+1"/>
         <source>Large Emoji in timeline</source>
-        <translation>Grandes émoticônes dans la discussion</translation>
+        <translation>Grands emojis dans la discussion</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Make font size larger if messages with only a few emojis are displayed.</source>
-        <translation>Augmente la taille de la police lors de l&apos;affichage de messages contenant uniquement quelques emojis.</translation>
+        <translation>Augmente la taille de la police des messages contenant uniquement quelques emojis.</translation>
+    </message>
+    <message>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>N&apos;envoyer des messages chiffrés qu&apos;aux utilisateurs vérifiés</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Requiert qu&apos;un utilisateur soit vérifié pour lui envoyer des messages chiffrés. La sécurité en est améliorée, mais le chiffrement de bout en bout devient plus fastidieux.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation>Partager vos clés avec les utilisateurs et appareils que vous avez vérifiés</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Répond automatiquement aux requêtes de clefs des autres utilisateurs, s&apos;ils sont vérifiés, même si cet appareil ne devrait pas avoir accès à ces clefs autrement.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Sauvegarde des clefs en ligne</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Télécharge les clefs de chiffrement de message depuis et envoie vers la sauvegarde chiffrée en ligne de clefs.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Activer la sauvegarde de clefs en ligne</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>Les auteurs de Nheko ne recommandent pas d&apos;activer la sauvegarde en ligne de clefs jusqu&apos;à ce que la sauvegarde symétrique en ligne de clefs soit disponible. Activer quand même ?</translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation>EN CACHE</translation>
     </message>
@@ -2259,14 +2802,14 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
         <translation>PAS DANS LE CACHE</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Facteur d&apos;échelle</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the scale factor of the whole user interface.</source>
-        <translation>Agrandit l&apos;interface entière de ce facteur.</translation>
+        <translation>Agrandit l&apos;interface entière par ce facteur.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2334,7 +2877,7 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
         <translation>Empreinte de l&apos;appareil</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Clés de session</translation>
     </message>
@@ -2354,39 +2897,39 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
         <translation>CHIFFREMENT</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>GÉNÉRAL</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>INTERFACE</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Joue les images comme les GIFs ou WEBPs uniquement quand la souris est au-dessus.</translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation>Mode écran tactile</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will prevent text selection in the timeline to make touch scrolling easier.</source>
-        <translation>Empêchera la sélection de texte dans la discussion pour faciliter le défilement tactile.</translation>
+        <translation>Empêche la sélection de texte dans la discussion pour faciliter le défilement tactile.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Emoji Font Family</source>
-        <translation>Nom de Police Emoji</translation>
-    </message>
-    <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation>Automatiquement répondre aux demandes de clés de déchiffrement des autres utilisateurs, si ceux-ci sont vérifiés.</translation>
+        <translation>Nom de police pour émoji</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
-        <translation>Clé de signature de l&apos;utilisateur</translation>
+        <translation>Clé de signature maîtresse de l&apos;utilisateur</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2401,12 +2944,12 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
     <message>
         <location line="+2"/>
         <source>The key to verify other users. If it is cached, verifying a user will verify all their devices.</source>
-        <translation type="unfinished">La clé utilisée pour vérifier d&apos;autres utilisateurs. Si celle-ci est cachée, vérifier un utilisateur vérifiera tous ses appareils.</translation>
+        <translation>La clé utilisée pour vérifier d&apos;autres utilisateurs. Si celle-ci est dans le cache, vérifier un utilisateur vérifiera tous ses appareils.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
-        <translation type="unfinished">Clé d&apos;auto-vérification</translation>
+        <translation>Clé d&apos;auto-vérification</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2416,12 +2959,12 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
     <message>
         <location line="+3"/>
         <source>Backup key</source>
-        <translation type="unfinished">Clé de secours</translation>
+        <translation>Clé de secours</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server.</source>
-        <translation>La clé utilisée pour déchiffrer les sauvegardes de clé stockées en ligne. Si celle-ci est cachée, vous pouvez activer la sauvegarde de vos clés en ligne afin d&apos;en conserver une copie chiffrée en toute sécurité sur le serveur.</translation>
+        <translation>La clé utilisée pour déchiffrer les sauvegardes de clé stockées en ligne. Si celle-ci est dans le cache, vous pouvez activer la sauvegarde de vos clés en ligne afin d&apos;en conserver une copie chiffrée en toute sécurité sur le serveur.</translation>
     </message>
     <message>
         <location line="+54"/>
@@ -2434,14 +2977,14 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
         <translation>Tous les types de fichiers (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
-        <translation>Ouvrir fichier de sessions</translation>
+        <translation>Ouvrir le fichier de sessions</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2449,26 +2992,26 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
         <translation>Erreur</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Mot de passe du fichier</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
-        <translation>Entrez la clé secrète pour déchiffrer le fichier&#xa0;&#xa0;:</translation>
+        <translation>Entrez la phrase de passe pour déchiffrer le fichier&#xa0;:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>Le mot de passe ne peut être vide</translation>
     </message>
     <message>
         <location line="-8"/>
         <source>Enter passphrase to encrypt your session keys:</source>
-        <translation>Entrez une clé secrète pour chiffrer vos clés de session&#xa0;&#xa0;:</translation>
+        <translation>Entrez une phrase de passe pour chiffrer vos clés de session&#xa0;:</translation>
     </message>
     <message>
         <location line="+15"/>
@@ -2476,6 +3019,14 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
         <translation>Fichier où sauvegarder les clés de session exportées</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Aucune discussion privée chiffrée trouvée avec cet utilisateur. Créez-en une et réessayez.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2583,7 +3134,7 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
     <message>
         <location filename="../../src/dialogs/FallbackAuth.cpp" line="+34"/>
         <source>Open Fallback in Browser</source>
-        <translation>Ouvrir la solution de repli dans le navigateur</translation>
+        <translation>Ouvrir la solution de secours dans le navigateur</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2598,38 +3149,7 @@ Cela met l&apos;application en évidence dans la barre des tâches.</translation
     <message>
         <location line="+12"/>
         <source>Open the fallback, follow the steps and confirm after completing them.</source>
-        <translation>Ouvrez la solution de repli, suivez les étapes et confirmez après les avoir terminées.</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Rejoindre</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Annuler</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Identifiant ou alias du salon</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Annuler</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Êtes-vous sûr·e de vouloir quitter ?</translation>
+        <translation>Ouvrez la solution de secours, suivez les étapes et confirmez après les avoir terminées.</translation>
     </message>
 </context>
 <context>
@@ -2685,32 +3205,6 @@ Taille du média : %2
         <translation>Résolvez le reCAPTCHA puis appuyez sur le bouton de confirmation</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Accusés de lecture</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Fermer</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Aujourd&apos;hui %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Hier %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2724,47 +3218,47 @@ Taille du média : %2
         <translation>%1 a envoyé un message audio</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Vous avez envoyé une image</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 a envoyé une image</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Vous avez envoyé un fichier</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 a envoyé un fichier</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Vous avez envoyé une vidéo</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 a envoyé une vidéo</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Vous avez envoyé un autocollant</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 a envoyé un autocollant</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Vous avez envoyé une notification</translation>
     </message>
@@ -2776,12 +3270,12 @@ Taille du média : %2
     <message>
         <location line="+5"/>
         <source>You: %1</source>
-        <translation>Vous&#xa0;&#xa0;: %1</translation>
+        <translation>Vous&#xa0;: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
-        <translation>%1&#xa0;&#xa0;: %2</translation>
+        <translation>%1&#xa0;: %2</translation>
     </message>
     <message>
         <location line="+7"/>
@@ -2799,27 +3293,27 @@ Taille du média : %2
         <translation>Vous avez appelé</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 a appelé</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Vous avez répondu à un appel</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 a répondu à un appel</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Vous avez terminé un appel</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 a terminé un appel</translation>
     </message>
@@ -2827,7 +3321,7 @@ Taille du média : %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Type du message inconnu</translation>
     </message>
diff --git a/resources/langs/nheko_hu.ts b/resources/langs/nheko_hu.ts
index 7c29338c2c19a996eff89a18649e932e6185adaa..ffef514be53d163929e1d8999439c72faef9faf9 100644
--- a/resources/langs/nheko_hu.ts
+++ b/resources/langs/nheko_hu.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Hívás...</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Videóhívás</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Videóhívás</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Az egész képernyő</translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Nem sikerült meghívni a felhasználót: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>A felhasználó meg lett hívva: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>A gyorsítótár átvitele a jelenlegi verzióhoz nem sikerült. Ennek több oka is lehet. Kérlek, írj egy hibajelentést és egyelőre próbálj meg egy régebbi verziót használni! Alternatív megoldásként megprobálhatod eltávolítani a gyorsítótárat kézzel.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Csatlakozás megerősítése</translation>
     </message>
@@ -151,23 +151,23 @@
         <translation>Biztosan csatlakozni akarsz a(z) %1 szobához?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>A %1 nevű szoba létre lett hozva.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Meghívás megerősítése</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Biztos, hogy meg akarod hívni a következő felhasználót: %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Nem sikerült %1 meghívása a(z) %2 szobába: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Biztosan ki akarod rúgni %1 (%2) felhasználót?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Kirúgott felhasználó: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation>Biztosan ki akarod tiltani %1 (%2) felhasználót?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Nem sikerült kitiltani %1 felhasználót a %2 szobából: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation>Biztosan fel akarod oldani %1 (%2) felhasználó kitiltását?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Nem sikerült feloldani %1 felhasználó kitiltását a %2 szobából: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Kitiltás feloldva a felhasználónak: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation>Biztosan privát csevegést akarsz indítani %1 felhasználóval?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Gyorsítótár migráció nem sikerült!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>A lemezeden lévő gyorsítótár újabb, mint amit a Nheko jelenlegi verziója támogat. Kérlek, frissítsd vagy töröld a gyorsítótárat!</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Nem sikerült visszaállítani az OLM fiókot. Kérlek, jelentkezz be ismét!</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Nem sikerült visszaállítani a mentési adatot. Kérlek, jelentkezz be ismét!</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Nem sikerült beállítani a titkosítási kulcsokat. Válasz a szervertől: %1 %2. Kérlek, próbáld újra később!</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Kérlek, próbálj meg bejelentkezni újra: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Nem sikerült csatlakozni a szobához: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Csatlakoztál a szobához</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Nem sikerült eltávolítani a meghívót: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Nem sikerült létrehozni a szobát: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Nem sikerült elhagyni a szobát: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation>Nem sikerült kirúgni %1 felhasználót %2 szobából: %3</translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Titkos tároló feloldása</translation>
     </message>
@@ -362,12 +364,12 @@
         <translation>Add meg a helyreállítási kulcsodat vagy a jelmondatodat a titkos tároló feloldásához:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation>Add meg a %1 nevű helyreállítási kulcsodat vagy a jelmondatodat a titkos tároló feloldásához:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Titkosítás feloldása nem sikerült</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Keresés</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Emberek</translation>
     </message>
@@ -495,71 +497,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Ez az üzenet nincs titkosítva!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>-- Titkosított esemény (Nem találhatók kulcsok a titkosítás feloldásához) --</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation>-- Titkosított esemény (a kulcs nem érvényes ehhez az indexhez) --</translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>-- Hiba a titkosítás feloldásakor (nem sikerült lekérni a megolm kulcsokat az adatbázisból) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Hiba a titkosítás feloldásakor (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Ez az üzenet nincs titkosítva!</translation>
     </message>
     <message>
-        <location line="-52"/>
-        <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>-- Titkosított esemény (Ismeretlen eseménytípus) --</translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Újrajátszási támadás! Ez az üzenetindex újra fel lett használva! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Nem hitelesített eszközről érkezett üzenet! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Időtúllépés az eszközhitelesítés alatt.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>A másik fél megszakította a hitelesítést.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Bezárás</translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">Mégse</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished">Szerkesztés</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished">Bezárás</translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Fájl kiválasztása</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation>Minden fájl (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Nem sikerült feltölteni a médiafájlt. Kérlek, próbáld újra!</translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">Mégse</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Szoba azonosítója vagy álneve</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Szoba elhagyása</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Biztosan távozni akarsz?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -760,25 +885,25 @@ Példa: https://szerver.em:8787</translation>
         <translation>BEJELENTKEZÉS</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation>Érvénytelen Matrixazonosítót adtál meg. Példa: @janos:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Az automatikus felderítés nem sikerült. Helytelen válasz érkezett.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Az automatikus felderítés nem sikerült. Ismeretlen hiba a .well-known lekérése közben.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Nem találhatók szükséges végpontok. Lehet, hogy nem egy Matrixszerver.</translation>
     </message>
@@ -788,35 +913,53 @@ Példa: https://szerver.em:8787</translation>
         <translation>Helytelen válasz érkezett. Ellenőrizd, hogy a homeszervered domainje helyes.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Egy ismeretlen hiba történt. Ellenőrizd, hogy a homeszervered domainje helyes.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>SSO BEJELENTKEZÉS</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Üres jelszó</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>SSO bejelentkezés nem sikerült</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation>Titkosítás bekapcsolva</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>a szoba neve megváltoztatva erre: %1</translation>
     </message>
@@ -866,18 +1009,23 @@ Példa: https://szerver.em:8787</translation>
         <translation>Hívás előkészítése…</translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation>%1 fogadta a hívást.</translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>eltávolítva</translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation>%1 befejezte a hívást.</translation>
     </message>
@@ -905,7 +1053,7 @@ Példa: https://szerver.em:8787</translation>
         <translation>Írj egy üzenetet…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -928,7 +1076,7 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Szerkesztés</translation>
     </message>
@@ -948,17 +1096,19 @@ Példa: https://szerver.em:8787</translation>
         <translation>Műveletek</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1017,6 +1167,11 @@ Példa: https://szerver.em:8787</translation>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Példa: https://szerver.em:8787</translation>
         <translation>Hitelesítési kérés érkezett</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation>Hogy mások láthassák, melyik eszköz tartozik valóban hozzád, hitelesíteni tudod őket. Ez arra is lehetőséget ad, hogy automatikus biztonsági másolat készüljön a kulcsokról. Hitelesíted a %1 nevű eszközt most?</translation>
     </message>
@@ -1076,33 +1236,29 @@ Példa: https://szerver.em:8787</translation>
         <translation>Elfogadás</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 küldött egy titkosított üzenetet</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation>* %1 %2</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>%1 válasza: %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation>%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,7 +1289,7 @@ Példa: https://szerver.em:8787</translation>
         <translation>Nem található mikrofon.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Hang</translation>
     </message>
@@ -1164,7 +1320,7 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation>Egy egyedi profil létrehozása, amellyel be tudsz jelentkezni egyszerre több fiókon keresztül és a Nheko több példányát is tudod futtatni.</translation>
     </message>
@@ -1179,21 +1335,37 @@ Példa: https://szerver.em:8787</translation>
         <translation>profilnév</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Olvasási jegyek</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Felhasználónév</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>A felhasználónév nem lehet üres és csak a következő karaktereket tartalmazhatja: a-z, 0-9, ., _, =, - és /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Jelszó</translation>
     </message>
@@ -1213,7 +1385,7 @@ Példa: https://szerver.em:8787</translation>
         <translation>Homeszerver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Egy szerver, amelyen engedélyezve vannak a regisztrációk. Mivel a Matrix decentralizált, először találnod kell egy szervert, ahol regisztrálhatsz, vagy be kell állítanod a saját szervered.</translation>
     </message>
@@ -1223,27 +1395,17 @@ Példa: https://szerver.em:8787</translation>
         <translation>REGISZTRÁCIÓ</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Nem támogatott regisztrációs folyamat!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>Egy vagy több mező tartalma nem helyes. Kérlek, javítsd ki azokat a hibákat, és próbáld újra!</translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished">Az automatikus felderítés nem sikerült. Helytelen válasz érkezett.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished">Az automatikus felderítés nem sikerült. Ismeretlen hiba a .well-known lekérése közben.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished">Nem találhatók szükséges végpontok. Lehet, hogy nem egy Matrixszerver.</translation>
     </message>
@@ -1258,17 +1420,17 @@ Példa: https://szerver.em:8787</translation>
         <translation type="unfinished">Egy ismeretlen hiba történt. Ellenőrizd, hogy a homeszervered domainje helyes.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>A jelszó nem elég hosszú (legalább 8 karakter)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>A jelszavak nem egyeznek</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Nem megfelelő szervernév</translation>
     </message>
@@ -1276,7 +1438,7 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Bezárás</translation>
     </message>
@@ -1287,33 +1449,41 @@ Példa: https://szerver.em:8787</translation>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
-        <translation>nincs tárolva verzió</translation>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>RoomInfo</name>
     <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation>nincs tárolva verzió</translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1347,7 +1517,7 @@ Példa: https://szerver.em:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1367,12 +1537,35 @@ Példa: https://szerver.em:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished">Kijelentkezés</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Bezárás</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished">Új csevegés indítása</translation>
     </message>
@@ -1392,7 +1585,7 @@ Példa: https://szerver.em:8787</translation>
         <translation type="unfinished">Szobák jegyzéke</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished">Felhasználói beállítások</translation>
     </message>
@@ -1400,12 +1593,12 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1417,16 +1610,36 @@ Példa: https://szerver.em:8787</translation>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation>Szobabeállítások</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation>%1 tag</translation>
     </message>
@@ -1456,7 +1669,12 @@ Példa: https://szerver.em:8787</translation>
         <translation>Az összes üzenet</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation>Bárki és vendégek</translation>
     </message>
@@ -1471,7 +1689,17 @@ Példa: https://szerver.em:8787</translation>
         <translation>Meghívott felhasználók</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Titkosítás</translation>
     </message>
@@ -1517,12 +1745,12 @@ Példa: https://szerver.em:8787</translation>
         <translation>Szoba verziója</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation>Nem sikerült a titkosítás aktiválása: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Profilkép kiválasztása</translation>
     </message>
@@ -1542,8 +1770,8 @@ Példa: https://szerver.em:8787</translation>
         <translation>Hiba a fájl olvasása közben: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation>Nem sikerült a kép feltöltése: %s</translation>
     </message>
@@ -1551,21 +1779,49 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1620,6 +1876,121 @@ Példa: https://szerver.em:8787</translation>
         <translation>Mégse</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1672,18 +2043,18 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Az üzenet visszavonása nem sikerült: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation>Nem sikerült titkosítani az eseményt, küldés megszakítva!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Kép mentése</translation>
     </message>
@@ -1703,7 +2074,7 @@ Példa: https://szerver.em:8787</translation>
         <translation>Fájl mentése</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1711,7 +2082,7 @@ Példa: https://szerver.em:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 nyilvánosan elérhetővé tette a szobát.</translation>
     </message>
@@ -1721,7 +2092,17 @@ Példa: https://szerver.em:8787</translation>
         <translation>%1 beállította, hogy meghívással lehessen csatlakozni ehhez a szobához.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 elérhetővé tette a szobát vendégeknek.</translation>
     </message>
@@ -1741,12 +2122,12 @@ Példa: https://szerver.em:8787</translation>
         <translation>%1 beállította, hogy a szoba előzményei ezentúl csak a tagok számára legyenek láthatóak.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 beállította, hogy a szoba előzményei láthatóak legyenek a tagok számára a meghívásuktól kezdve.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 beállította, hogy a szoba előzményei láthatóak legyenek a tagok számára a csatlakozásuktól kezdve.</translation>
     </message>
@@ -1756,12 +2137,12 @@ Példa: https://szerver.em:8787</translation>
         <translation>%1 megváltoztatta a szoba engedélyeit.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 meg lett hívva.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 megváltoztatta a profilképét.</translation>
     </message>
@@ -1771,12 +2152,17 @@ Példa: https://szerver.em:8787</translation>
         <translation>%1 megváltoztatta a profiladatait.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 csatlakozott.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 elutasította a meghívását.</translation>
     </message>
@@ -1806,32 +2192,32 @@ Példa: https://szerver.em:8787</translation>
         <translation>%1 ki lett tiltva.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 visszavonta a kopogását.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Csatlakoztál ehhez a szobához.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Kopogás elutasítva tőle: %1.</translation>
     </message>
@@ -1850,7 +2236,7 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Szerkesztve</translation>
     </message>
@@ -1858,12 +2244,17 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Nincs nyitott szoba</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished">%1 tag</translation>
     </message>
@@ -1888,28 +2279,40 @@ Példa: https://szerver.em:8787</translation>
         <translation type="unfinished">Vissza a szobák listájára</translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation>Nem található titkosított privát csevegés ezzel a felhasználóval. Hozz létre egy titkosított privát csevegést vele, és próbáld újra!</translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation>Vissza a szobák listájára</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation>Nincs kiválasztva szoba</translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation>Szoba beállításai</translation>
     </message>
@@ -1947,10 +2350,35 @@ Példa: https://szerver.em:8787</translation>
         <translation>Kilépés</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation>Globális felhasználói profil</translation>
     </message>
@@ -1960,33 +2388,98 @@ Példa: https://szerver.em:8787</translation>
         <translation>Szobai felhasználói profil</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
         <translation>Hitelesítés</translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
-        <translation>A felhasználó tiltása</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation>Privát csevegés indítása</translation>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
-        <translation>A felhasználó kirúgása</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation>Hitelesítés visszavonása</translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Profilkép kiválasztása</translation>
     </message>
@@ -2009,8 +2502,8 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation>Alapértelmezett</translation>
     </message>
@@ -2018,7 +2511,7 @@ Példa: https://szerver.em:8787</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Kicsinyítés a tálcára</translation>
     </message>
@@ -2028,22 +2521,22 @@ Példa: https://szerver.em:8787</translation>
         <translation>Indítás a tálcán</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Csoport oldalsávja</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Kerekített profilképek</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation>profil: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation>Alapértelmezett</translation>
     </message>
@@ -2068,7 +2561,7 @@ Példa: https://szerver.em:8787</translation>
         <translation>LETÖLTÉS</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation>Az alkalmazás azután is a háttérben fut, miután be lett zárva a főablak.</translation>
     </message>
@@ -2084,6 +2577,16 @@ OFF - square, ON - Circle.</source>
         <translation>A profilképek megjelenése a csevegésekben.
 KI - szögletes, BE - kerek.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2114,7 +2617,7 @@ be blurred.</source>
 az idővonal homályosítva lesz.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation>Idővonal kitakarása ennyi idő után (másodpercben, 0 és 3600 között)</translation>
     </message>
@@ -2175,7 +2678,7 @@ Ha ki van kapcsolva, a szobák sorrendje csak a bennük lévő utolsó üzenet d
 Ha be van kapcsolva, azok a szobák kerülnek felülre, amelyekhez aktív értesítés tartozik (amelyet a számot tartalmazó kis kör jelez). A némított szobák továbbra is dátum alapján lesznek rendezve, mivel nem valószínű, hogy ezeket annyira fontosnak tartod, mint a többi szobát.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Olvasási jegyek</translation>
     </message>
@@ -2187,7 +2690,7 @@ Status is displayed next to timestamps.</source>
 Ez az állapot az üzenetek ideje mellett jelenik meg.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Üzenetek küldése Markdownként</translation>
     </message>
@@ -2200,6 +2703,11 @@ Ha ki van kapcsolva, az összes üzenet sima szövegként lesz elküldve.</trans
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Asztali értesítések</translation>
     </message>
@@ -2241,12 +2749,47 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>A betűméret megnövelése, ha az üzenetek csak néhány hangulatjelet tartalmaznak.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation>Kulcsok megosztása hitelesített felhasználókkal és eszközökkel</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation>GYORSÍTÓTÁRAZVA</translation>
     </message>
@@ -2256,7 +2799,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>NINCS GYORSÍTÓTÁRAZVA</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Nagyítási tényező</translation>
     </message>
@@ -2331,7 +2874,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>Eszközujjlenyomat</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Munkamenetkulcsok</translation>
     </message>
@@ -2351,17 +2894,22 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>TITKOSÍTÁS</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>ÁLTALÁNOS</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>FELÜLET</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation>Érintő képernyős mód</translation>
     </message>
@@ -2376,12 +2924,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>Hangulatjelek betűtípusa</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation>Automatikus válasz a más felhasználóktól érkező kulcskérelmekre, ha ők hitelesítve vannak.</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation>Mester-aláírókulcs</translation>
     </message>
@@ -2401,7 +2944,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>A mások hitelesítésére használt kulcs. Ha gyorsítótárazva van, egy felhasználó hitelesítésekor hitelesítve lesz az összes eszköze.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation>Önaláírókulcs</translation>
     </message>
@@ -2431,14 +2974,14 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>Minden fájl (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Munkameneti fájl megnyitása</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2446,19 +2989,19 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>Hiba</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Fájljelszó</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Írd be a jelmondatot a fájl titkosításának feloldásához:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>A jelszó nem lehet üres</translation>
     </message>
@@ -2473,6 +3016,14 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>Exportált munkameneti kulcsok mentése fájlba</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Nem található titkosított privát csevegés ezzel a felhasználóval. Hozz létre egy titkosított privát csevegést vele, és próbáld újra!</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2598,37 +3149,6 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő
         <translation>Nyisd meg a fallback-ket, kövesd az utasításokat, és erősítsd meg, ha végeztél velük!</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Csatlakozás</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Mégse</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Szoba azonosítója vagy álneve</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Mégse</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Biztosan távozni akarsz?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2682,32 +3202,6 @@ Média mérete: %2
         <translation>Oldd meg a reCAPTCHA feladványát, és nyomd meg a „Megerősítés” gombot</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Olvasási jegyek</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Bezárás</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Ma %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Tegnap %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2721,47 +3215,47 @@ Média mérete: %2
         <translation>%1 küldött egy hangfájlt</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Küldtél egy képet</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 küldött egy képet</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Küldtél egy fájlt</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 küldtél egy fájlt</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Küldtél egy videót</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 küldött egy videót</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Küldtél egy matricát</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 küldött egy matricát</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Küldtél egy értesítést</translation>
     </message>
@@ -2776,7 +3270,7 @@ Média mérete: %2
         <translation>Te: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2796,27 +3290,27 @@ Média mérete: %2
         <translation>Hívást kezdeményeztél</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 hívást kezdeményezett</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Fogadtál egy hívást</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 hívást fogadott</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Befejeztél egy hívást</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 befejezett egy hívást</translation>
     </message>
@@ -2824,7 +3318,7 @@ Média mérete: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Ismeretlen üzenettípus</translation>
     </message>
diff --git a/resources/langs/nheko_id.ts b/resources/langs/nheko_id.ts
new file mode 100644
index 0000000000000000000000000000000000000000..accb1eabe8fba3fdf05c40292bddef266b334e72
--- /dev/null
+++ b/resources/langs/nheko_id.ts
@@ -0,0 +1,3325 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="id">
+<context>
+    <name>ActiveCallBar</name>
+    <message>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
+        <source>Calling...</source>
+        <translation>Memanggil...</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+10"/>
+        <source>Connecting...</source>
+        <translation>Menghubungkan...</translation>
+    </message>
+    <message>
+        <location line="+67"/>
+        <source>You are screen sharing</source>
+        <translation>Anda sedang membagikan layar</translation>
+    </message>
+    <message>
+        <location line="+17"/>
+        <source>Hide/Show Picture-in-Picture</source>
+        <translation>Sembunyikan/Tampilkan Picture-in-Picture</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Unmute Mic</source>
+        <translation>Bunyikan Mikrofon</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Mute Mic</source>
+        <translation>Bisukan Mikrofon</translation>
+    </message>
+</context>
+<context>
+    <name>AwaitingVerificationConfirmation</name>
+    <message>
+        <location filename="../qml/device-verification/AwaitingVerificationConfirmation.qml" line="+12"/>
+        <source>Awaiting Confirmation</source>
+        <translation>Menunggu Konfirmasi</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Waiting for other side to complete verification.</source>
+        <translation>Menunggu untuk pengguna yang lain untuk menyelesaikan verifikasi.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Cancel</source>
+        <translation>Batal</translation>
+    </message>
+</context>
+<context>
+    <name>CallInvite</name>
+    <message>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
+        <source>Video Call</source>
+        <translation>Panggilan Video</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Voice Call</source>
+        <translation>Panggilan Suara</translation>
+    </message>
+    <message>
+        <location line="+62"/>
+        <source>No microphone found.</source>
+        <translation>Tidak ada mikrofon yang ditemukan.</translation>
+    </message>
+</context>
+<context>
+    <name>CallInviteBar</name>
+    <message>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
+        <source>Video Call</source>
+        <translation>Panggilan Video</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Voice Call</source>
+        <translation>Panggilan Suara</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Devices</source>
+        <translation>Perangkat</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Accept</source>
+        <translation>Terima</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Unknown microphone: %1</source>
+        <translation>Mikrofon tidak dikenal: %1</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Unknown camera: %1</source>
+        <translation>Kamera tidak dikenal: %1</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Decline</source>
+        <translation>Tolak</translation>
+    </message>
+    <message>
+        <location line="-28"/>
+        <source>No microphone found.</source>
+        <translation>Tidak ada mikrofon yang ditemukan.</translation>
+    </message>
+</context>
+<context>
+    <name>CallManager</name>
+    <message>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
+        <source>Entire screen</source>
+        <translation>Semua layar</translation>
+    </message>
+</context>
+<context>
+    <name>ChatPage</name>
+    <message>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
+        <source>Failed to invite user: %1</source>
+        <translation>Gagal mengundang pengguna: %1</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <location line="+662"/>
+        <source>Invited user: %1</source>
+        <translation>Pengguna yang diundang: %1</translation>
+    </message>
+    <message>
+        <location line="-448"/>
+        <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
+        <translation>Migrasi cache ke versi saat ini gagal. Ini dapat memiliki alasan yang berbeda. Silakan buka masalah dan coba gunakan versi yang lebih lama untuk sementara. Alternatifnya Anda dapat mencoba menghapus cache secara manual.</translation>
+    </message>
+    <message>
+        <location line="+355"/>
+        <source>Confirm join</source>
+        <translation>Konfirmasi untuk bergabung</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Do you really want to join %1?</source>
+        <translation>Apakah Anda ingin bergabung %1?</translation>
+    </message>
+    <message>
+        <location line="+42"/>
+        <source>Room %1 created.</source>
+        <translation>Ruangan %1 telah dibuat.</translation>
+    </message>
+    <message>
+        <location line="+34"/>
+        <location line="+445"/>
+        <source>Confirm invite</source>
+        <translation>Konfirmasi undangan</translation>
+    </message>
+    <message>
+        <location line="-444"/>
+        <source>Do you really want to invite %1 (%2)?</source>
+        <translation>Apakah Anda ingin menundang %1 (%2)?</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Failed to invite %1 to %2: %3</source>
+        <translation>Gagal mengundang %1 ke %2: %3</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>Confirm kick</source>
+        <translation>Konfirmasi pengeluaran</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Do you really want to kick %1 (%2)?</source>
+        <translation>Apakah Anda ingin mengeluarkan %1 (%2)?</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>Kicked user: %1</source>
+        <translation>Pengguna yang dikeluarkan: %1</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Confirm ban</source>
+        <translation>Konfirmasi cekalan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Do you really want to ban %1 (%2)?</source>
+        <translation>Apakah Anda ingin mencekal %1 (%2)?</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Failed to ban %1 in %2: %3</source>
+        <translation>Gagal mencekal %1 di %2: %3</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Banned user: %1</source>
+        <translation>Pengguna yang dicekal: %1</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Confirm unban</source>
+        <translation>Konfirmasi menghilangkan cekalan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Do you really want to unban %1 (%2)?</source>
+        <translation>Apakah Anda ingin menghilangkan cekalan %1 (%2)?</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Failed to unban %1 in %2: %3</source>
+        <translation>Gagal menghilangkan pencekalan %1 di %2: %3</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Unbanned user: %1</source>
+        <translation>Menghilangkan cekalan pengguna: %1</translation>
+    </message>
+    <message>
+        <location line="+352"/>
+        <source>Do you really want to start a private chat with %1?</source>
+        <translation>Apakah Anda ingin memulai chat privat dengan %1?</translation>
+    </message>
+    <message>
+        <location line="-879"/>
+        <source>Cache migration failed!</source>
+        <translation>Migrasi cache gagal!</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Incompatible cache version</source>
+        <translation>Versi cache tidak kompatibel</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache.</source>
+        <translation>Cache pada disk Anda lebih baru daripada versi yang didukung Nheko ini. Harap perbarui atau kosongkan cache Anda.</translation>
+    </message>
+    <message>
+        <location line="+49"/>
+        <source>Failed to restore OLM account. Please login again.</source>
+        <translation>Gagal memulihkan akun OLM. Mohon masuk lagi.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <location line="+4"/>
+        <location line="+4"/>
+        <source>Failed to restore save data. Please login again.</source>
+        <translation>Gagal memulihkan data simpanan. Mohon masuk lagi.</translation>
+    </message>
+    <message>
+        <location line="+93"/>
+        <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
+        <translation>Gagal menyiapkan kunci enkripsi. Respons server: %1 %2. Silakan coba lagi nanti.</translation>
+    </message>
+    <message>
+        <location line="+32"/>
+        <location line="+115"/>
+        <source>Please try to login again: %1</source>
+        <translation>Mohon mencoba masuk lagi: %1</translation>
+    </message>
+    <message>
+        <location line="+49"/>
+        <source>Failed to join room: %1</source>
+        <translation>Gagal bergabung ruangan: %1</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You joined the room</source>
+        <translation>Anda bergabung ruangan ini</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Failed to remove invite: %1</source>
+        <translation>Gagal menghapus undangan: %1</translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Room creation failed: %1</source>
+        <translation>Pembuatan ruangan gagal: %1</translation>
+    </message>
+    <message>
+        <location line="+18"/>
+        <source>Failed to leave room: %1</source>
+        <translation>Gagal meninggalkan ruangan: %1</translation>
+    </message>
+    <message>
+        <location line="+58"/>
+        <source>Failed to kick %1 from %2: %3</source>
+        <translation>Gagal mengeluarkan %1 dari %2: %3</translation>
+    </message>
+</context>
+<context>
+    <name>CommunitiesList</name>
+    <message>
+        <location filename="../qml/CommunitiesList.qml" line="+44"/>
+        <source>Hide rooms with this tag or from this space by default.</source>
+        <translation>Sembunyikan ruangan dengan tanda ini atau dari space ini secara default.</translation>
+    </message>
+</context>
+<context>
+    <name>CommunitiesModel</name>
+    <message>
+        <location filename="../../src/timeline/CommunitiesModel.cpp" line="+37"/>
+        <source>All rooms</source>
+        <translation>Semua ruangan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Shows all rooms without filtering.</source>
+        <translation>Menampilkan semua ruangan tanpa penyaringan.</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Favourites</source>
+        <translation>Favorit</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Rooms you have favourited.</source>
+        <translation>Ruangan yang Anda favoritkan.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Low Priority</source>
+        <translation>Prioritas Rendah</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Rooms with low priority.</source>
+        <translation>Ruangan dengan prioritas rendah.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Server Notices</source>
+        <translation>Pemberitahuan Server</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Messages from your server or administrator.</source>
+        <translation>Pesan dari server Anda atau administrator.</translation>
+    </message>
+</context>
+<context>
+    <name>CrossSigningSecrets</name>
+    <message>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
+        <source>Decrypt secrets</source>
+        <translation>Dekripsi rahasia</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Enter your recovery key or passphrase to decrypt your secrets:</source>
+        <translation>Masukkan kunci pemulihan Anda atau frasa sandi untuk mendekripsikan rahasia Anda:</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
+        <translation>Masukkan kunci pemulihan Anda atau frasa sandi yang bernama %1 untuk mendekripsikan rahasia Anda:</translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Decryption failed</source>
+        <translation>Gagal mendekripsi</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Failed to decrypt secrets with the provided recovery key or passphrase</source>
+        <translation>Gagal mendekripsi rahasia dengan kunci pemulihan atau frasa sandi yang diberikan</translation>
+    </message>
+</context>
+<context>
+    <name>DigitVerification</name>
+    <message>
+        <location filename="../qml/device-verification/DigitVerification.qml" line="+11"/>
+        <source>Verification Code</source>
+        <translation>Kode Verifikasi</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please verify the following digits. You should see the same numbers on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
+        <translation>Harap verifikasi digit berikut.  Anda seharusnya melihat angka yang sama di kedua sisi.  Jika mereka berbeda, mohon tekan &apos;Mereka tidak cocok!&apos; untuk membatalkan verifikasi!</translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>They do not match!</source>
+        <translation>Mereka tidak cocok!</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>They match!</source>
+        <translation>Mereka cocok!</translation>
+    </message>
+</context>
+<context>
+    <name>EditModal</name>
+    <message>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+42"/>
+        <source>Apply</source>
+        <translation>Terapkan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Name</source>
+        <translation>Nama</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Topic</source>
+        <translation>Topik</translation>
+    </message>
+</context>
+<context>
+    <name>EmojiPicker</name>
+    <message>
+        <location filename="../qml/emoji/EmojiPicker.qml" line="+68"/>
+        <source>Search</source>
+        <translation>Cari</translation>
+    </message>
+    <message>
+        <location line="+187"/>
+        <source>People</source>
+        <translation>Orang</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Nature</source>
+        <translation>Alam</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Food</source>
+        <translation>Makanan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Activity</source>
+        <translation>Aktifitas</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Travel</source>
+        <translation>Tempat</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Objects</source>
+        <translation>Objek</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Symbols</source>
+        <translation>Simbol</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Flags</source>
+        <translation>Bendera</translation>
+    </message>
+</context>
+<context>
+    <name>EmojiVerification</name>
+    <message>
+        <location filename="../qml/device-verification/EmojiVerification.qml" line="+11"/>
+        <source>Verification Code</source>
+        <translation>Kode Verifikasi</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
+        <translation>Mohon verifikasi emoji berikut. Anda seharusnya melihat emoji yang sama di kedua sisi. Jika mereka berbeda, mohon tekan &apos;Mereka tidak cocok!&apos; untuk membatalkan verifikasi!</translation>
+    </message>
+    <message>
+        <location line="+376"/>
+        <source>They do not match!</source>
+        <translation>Mereka tidak cocok!</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>They match!</source>
+        <translation>Mereka cocok!</translation>
+    </message>
+</context>
+<context>
+    <name>Encrypted</name>
+    <message>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Tidak ada kunci untuk mengakses pesan ini. Kami telah meminta untuk kunci secara otomatis, tetapi Anda dapat meminta lagi jika Anda tidak sabar.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Pesan ini tidak dapat didekripsikan, karena kami hanya memiliki kunci untuk pesan baru. Anda dapat meminta akses ke pesan ini.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Sebuah kesalahan internal terjadi saat membaca kunci dekripsi dari basis data.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>There was an error decrypting this message.</source>
+        <translation>Sebuah error terjadi saat mendekripsikan pesan ini.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Pesan ini tidak dapat diuraikan.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>Kunci enkripsi telah digunakan lagi! Seseorang mungkin mencoba memasukkan pesan palsu ke chat ini!</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Error dekripsi yang tidak dikenal</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Minta kunci</translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Pesan ini tidak terenkripsi!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Terenkripsi oleh perangkat yang terverifikasi</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Terenkripsi oleh perangkat yang tidak diverifikasi, tetapi Anda mempercayai pengguna itu sejauh ini.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Terenkripsi oleh perangkat yang tidak diverifikasi atau kuncinya dari sumber yang tidak dipercayai seperti cadangan kunci.</translation>
+    </message>
+</context>
+<context>
+    <name>Failed</name>
+    <message>
+        <location filename="../qml/device-verification/Failed.qml" line="+11"/>
+        <source>Verification failed</source>
+        <translation>Verifikasi gagal</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>Other client does not support our verification protocol.</source>
+        <translation>Client yang lain tidak mendukung protokol verifikasi kami.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Key mismatch detected!</source>
+        <translation>Ketidakcocokan kunci terdeteksi!</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Device verification timed out.</source>
+        <translation>Waktu verifikasi perangkat habis.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Other party canceled the verification.</source>
+        <translation>Pengguna yang lain membatalkan proses verifikasi ini.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Close</source>
+        <translation>Tutup</translation>
+    </message>
+</context>
+<context>
+    <name>ForwardCompleter</name>
+    <message>
+        <location filename="../qml/ForwardCompleter.qml" line="+44"/>
+        <source>Forward Message</source>
+        <translation>Teruskan Pesan</translation>
+    </message>
+</context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Mengedit paket gambar</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Tambahkan gambar</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Stiker (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>Kunci keadaan</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Nama Paket</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Atribusi</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Gunakan sebagai Emoji</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Gunakan sebagai Stiker</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Kode Pendek</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Body</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Hapus dari paket</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Hapus</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Simpan</translation>
+    </message>
+</context>
+<context>
+    <name>ImagePackSettingsDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
+        <source>Image pack settings</source>
+        <translation>Pengaturan paket gambar</translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Buat paket untuk akun</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Paket ruangan baru</translation>
+    </message>
+    <message>
+        <location line="+21"/>
+        <source>Private pack</source>
+        <translation>Paket privat</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Pack from this room</source>
+        <translation>Paket dari ruangan ini</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Globally enabled pack</source>
+        <translation>Paket yang diaktifkan secara global</translation>
+    </message>
+    <message>
+        <location line="+66"/>
+        <source>Enable globally</source>
+        <translation>Aktifkan secara global</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Enables this pack to be used in all rooms</source>
+        <translation>Mengaktifkan paket ini untuk digunakan di semua ruangan</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Edit</translation>
+    </message>
+    <message>
+        <location line="+64"/>
+        <source>Close</source>
+        <translation>Tutup</translation>
+    </message>
+</context>
+<context>
+    <name>InputBar</name>
+    <message>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
+        <source>Select a file</source>
+        <translation>Pilih sebuah file</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>All Files (*)</source>
+        <translation>Semua File (*)</translation>
+    </message>
+    <message>
+        <location line="+474"/>
+        <source>Failed to upload media. Please try again.</source>
+        <translation>Gagal untuk mengunggah media. Silakan coba lagi.</translation>
+    </message>
+</context>
+<context>
+    <name>InviteDialog</name>
+    <message>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
+        <source>Invite users to %1</source>
+        <translation>Undang pengguna ke %1</translation>
+    </message>
+    <message>
+        <location line="+24"/>
+        <source>User ID to invite</source>
+        <translation>ID Pengguna untuk diundang</translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>@joe:matrix.org</source>
+        <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
+        <translation>@pengguna:matrix.org</translation>
+    </message>
+    <message>
+        <location line="+17"/>
+        <source>Add</source>
+        <translation>Tambahkan</translation>
+    </message>
+    <message>
+        <location line="+58"/>
+        <source>Invite</source>
+        <translation>Undang</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+</context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">ID ruangan atau alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Tinggalkan ruangan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Apakah Anda yakin untuk meninggalkan ruangan?</translation>
+    </message>
+</context>
+<context>
+    <name>LoginPage</name>
+    <message>
+        <location filename="../../src/LoginPage.cpp" line="+81"/>
+        <source>Matrix ID</source>
+        <translation>ID Matrix</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>e.g @joe:matrix.org</source>
+        <translation>mis. @pengguna:matrix.org</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :.
+You can also put your homeserver address there, if your server doesn&apos;t support .well-known lookup.
+Example: @user:server.my
+If Nheko fails to discover your homeserver, it will show you a field to enter the server manually.</source>
+        <translation>Nama login Anda. Sebuah MXID harus mulai dengan @ diikuti dengan ID pengguna. Setelah ID penggunanya Anda harus menambahkan nama server setelah :.
+Anda juga dapat memasukkan alamat homeserver Anda, jika server Anda tidak mendukung pencarian .well-known.
+Misalnya: @pengguna:server.my
+Jika Nheko gagal menemukan homeserver Anda, Nheko akan menampilkan kolom untuk memasukkan servernya secara manual.</translation>
+    </message>
+    <message>
+        <location line="+25"/>
+        <source>Password</source>
+        <translation>Kata Sandi</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Your password.</source>
+        <translation>Kata sandi Anda.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Device name</source>
+        <translation>Nama perangkat</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.</source>
+        <translation>Sebuah nama perangkat untuk perangkat ini, yang akan ditampilkan untuk yang lain, ketika memverifikasi perangkat Anda. Jika tidak dimasukkan nama perangkat yang default akan digunakan.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Homeserver address</source>
+        <translation>Alamat homeserver</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>server.my:8787</source>
+        <translation>server.my:8787</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The address that can be used to contact you homeservers client API.
+Example: https://server.my:8787</source>
+        <translation>Alamat yang dapat digunakan untuk menghubungi API klien homeserver Anda.
+Misalnya: https://server.my:8787</translation>
+    </message>
+    <message>
+        <location line="+19"/>
+        <source>LOGIN</source>
+        <translation>MASUK</translation>
+    </message>
+    <message>
+        <location line="+83"/>
+        <location line="+11"/>
+        <location line="+151"/>
+        <location line="+11"/>
+        <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
+        <translation>Anda telah memasukkan ID Matrix yang tidak valid  mis. @pengguna:matrix.org</translation>
+    </message>
+    <message>
+        <location line="-126"/>
+        <source>Autodiscovery failed. Received malformed response.</source>
+        <translation>Penemuan otomatis gagal. Menerima respons cacat.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
+        <translation>Penemuan otomatis gagal. Kesalahan yang tidak diketahu saat meminta .well-known.</translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>The required endpoints were not found. Possibly not a Matrix server.</source>
+        <translation>Titik akhir yang dibutuhkan tidak dapat ditemukan. Kemungkinan bukan server Matrix.</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Received malformed response. Make sure the homeserver domain is valid.</source>
+        <translation>Menerima respons cacat. Pastikan domain homeservernya valid.</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
+        <translation>Terjadi kesalahan yang tidak diketahui. Pastikan domain homeservernya valid.</translation>
+    </message>
+    <message>
+        <location line="-164"/>
+        <source>SSO LOGIN</source>
+        <translation>LOGIN SSO</translation>
+    </message>
+    <message>
+        <location line="+257"/>
+        <source>Empty password</source>
+        <translation>Kata sandi kosong</translation>
+    </message>
+    <message>
+        <location line="+55"/>
+        <source>SSO login failed</source>
+        <translation>Login SSO gagal</translation>
+    </message>
+</context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MessageDelegate</name>
+    <message>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
+        <source>Encryption enabled</source>
+        <translation>Enkripsi diaktifkan</translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>room name changed to: %1</source>
+        <translation>nama ruangan diganti ke: %1</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>removed room name</source>
+        <translation>nama ruangan dihapus</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>topic changed to: %1</source>
+        <translation>topik diganti ke: %1</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>removed topic</source>
+        <translation>topik dihapus</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>%1 changed the room avatar</source>
+        <translation>%1 mengubah avatar ruangan</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>%1 created and configured room: %2</source>
+        <translation>%1 membuat dan mengkonfigurasikan ruangan: %2</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>%1 placed a voice call.</source>
+        <translation>%1 melakukan panggilan suara.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 placed a video call.</source>
+        <translation>%1 melakukan panggilan suara.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 placed a call.</source>
+        <translation>%1 melakukan panggilan.</translation>
+    </message>
+    <message>
+        <location line="+38"/>
+        <source>Negotiating call...</source>
+        <translation>Negosiasi panggilan…</translation>
+    </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Izinkan mereka untuk masuk</translation>
+    </message>
+    <message>
+        <location line="-94"/>
+        <source>%1 answered the call.</source>
+        <translation>%1 menjawab panggilan.</translation>
+    </message>
+    <message>
+        <location line="-109"/>
+        <location line="+9"/>
+        <source>removed</source>
+        <translation>dihapus</translation>
+    </message>
+    <message>
+        <location line="+112"/>
+        <source>%1 ended the call.</source>
+        <translation>%1 mengakhir panggilan.</translation>
+    </message>
+</context>
+<context>
+    <name>MessageInput</name>
+    <message>
+        <location filename="../qml/MessageInput.qml" line="+44"/>
+        <source>Hang up</source>
+        <translation>Tutup panggilan</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Place a call</source>
+        <translation>Lakukan panggilan</translation>
+    </message>
+    <message>
+        <location line="+25"/>
+        <source>Send a file</source>
+        <translation>Kirim sebuah file</translation>
+    </message>
+    <message>
+        <location line="+50"/>
+        <source>Write a message...</source>
+        <translation>Ketik pesan…</translation>
+    </message>
+    <message>
+        <location line="+234"/>
+        <source>Stickers</source>
+        <translation>Stiker</translation>
+    </message>
+    <message>
+        <location line="+24"/>
+        <source>Emoji</source>
+        <translation>Emoji</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Send</source>
+        <translation>Kirim</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>You don&apos;t have permission to send messages in this room</source>
+        <translation>Anda tidak memiliki izin untuk mengirim pesan di ruangan ini</translation>
+    </message>
+</context>
+<context>
+    <name>MessageView</name>
+    <message>
+        <location filename="../qml/MessageView.qml" line="+88"/>
+        <source>Edit</source>
+        <translation>Edit</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>React</source>
+        <translation>Reaksi</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Reply</source>
+        <translation>Balas</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Options</source>
+        <translation>Opsi</translation>
+    </message>
+    <message>
+        <location line="+420"/>
+        <location line="+118"/>
+        <source>&amp;Copy</source>
+        <translation>&amp;Salin</translation>
+    </message>
+    <message>
+        <location line="-111"/>
+        <location line="+118"/>
+        <source>Copy &amp;link location</source>
+        <translation>Salin lokasi &amp;tautan</translation>
+    </message>
+    <message>
+        <location line="-110"/>
+        <source>Re&amp;act</source>
+        <translation>Re&amp;aksi</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Repl&amp;y</source>
+        <translation>Bala&amp;s</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>&amp;Edit</source>
+        <translation>&amp;Edit</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Read receip&amp;ts</source>
+        <translation>Lapor&amp;an terbaca</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>&amp;Forward</source>
+        <translation>&amp;Teruskan</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>&amp;Mark as read</source>
+        <translation>&amp;Tandai sebagai dibaca</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>View raw message</source>
+        <translation>Tampilkan pesan mentah</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>View decrypted raw message</source>
+        <translation>Tampilkan pesan terdekripsi mentah</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Remo&amp;ve message</source>
+        <translation>Hap&amp;us pesan</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>&amp;Save as</source>
+        <translation>&amp;Simpan sebagai</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>&amp;Open in external program</source>
+        <translation>&amp;Buka di program eksternal</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Copy link to eve&amp;nt</source>
+        <translation>Salin tautan ke peristi&amp;wa</translation>
+    </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>&amp;Pergi ke pesan yang dikutip</translation>
+    </message>
+</context>
+<context>
+    <name>NewVerificationRequest</name>
+    <message>
+        <location filename="../qml/device-verification/NewVerificationRequest.qml" line="+11"/>
+        <source>Send Verification Request</source>
+        <translation>Kirim Permintaan Verifikasi</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Received Verification Request</source>
+        <translation>Menerima Permintaan Verifikasi</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
+        <translation>Untuk mengizinkan pengguna yang lain untuk melihat, perangkat apa saja yang sebenarnya milik Anda, verifiksi mereka. Ini juga dapat membuat kunci cadangan bekerja secara otomatis. Verifikasi %1 sekarang?</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.</source>
+        <translation>Supaya tidak ada pengguna yang jahat yang bisa melihat komunikasi yang terenkripsi Anda dapat memverifikasi pengguna yang lain.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 has requested to verify their device %2.</source>
+        <translation>%1 telah meminta untuk memverifikasi perangkat %2 mereka.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 using the device %2 has requested to be verified.</source>
+        <translation>%1 yang menggunakan perangkat %2 meminta untuk diverifikasi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Your device (%1) has requested to be verified.</source>
+        <translation>Perangkat Anda (%1) meminta untuk diverifikasi.</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Deny</source>
+        <translation>Tolak</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Start verification</source>
+        <translation>Mulai verifikasi</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Accept</source>
+        <translation>Terima</translation>
+    </message>
+</context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
+    <message>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
+        <translation>%1 mengirim pesan terenkripsi</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>%1 replied: %2</source>
+        <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
+        <translation>%1 membalas: %2</translation>
+    </message>
+    <message>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
+        <source>%1 replied with an encrypted message</source>
+        <translation>%1 membalas dengan pesan terenkripsi</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>%1 replied to a message</source>
+        <translation>%1 membalas pesan</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>%1 sent a message</source>
+        <translation>%1 mengirim gambar</translation>
+    </message>
+</context>
+<context>
+    <name>PlaceCall</name>
+    <message>
+        <location filename="../qml/voip/PlaceCall.qml" line="+48"/>
+        <source>Place a call to %1?</source>
+        <translation>Lakukan panggilan ke %1?</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>No microphone found.</source>
+        <translation>Tidak ada mikrofon yang ditemukan.</translation>
+    </message>
+    <message>
+        <location line="+23"/>
+        <source>Voice</source>
+        <translation>Suara</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Video</source>
+        <translation>Video</translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Screen</source>
+        <translation>Layar</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+</context>
+<context>
+    <name>Placeholder</name>
+    <message>
+        <location filename="../qml/delegates/Placeholder.qml" line="+11"/>
+        <source>unimplemented event: </source>
+        <translation>peristiwa yang belum diimplementasikan: </translation>
+    </message>
+</context>
+<context>
+    <name>QCoreApplication</name>
+    <message>
+        <location filename="../../src/main.cpp" line="+191"/>
+        <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
+        <translation>Membuat profil yang unik, yang mengizinkan Anda untuk masuk ke beberapa akun pada waktu bersamaan dan mulai beberapa instansi nheko.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>profile</source>
+        <translation>profil</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>profile name</source>
+        <translation>nama profil</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Laporan dibaca</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Kemarin, %1</translation>
+    </message>
+</context>
+<context>
+    <name>RegisterPage</name>
+    <message>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
+        <source>Username</source>
+        <translation>Nama pengguna</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <location line="+147"/>
+        <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
+        <translation>Nama pengguna tidak boleh kosong, dan hanya mengandung karakter a-z, 0-9, ., _, =, -, dan /.</translation>
+    </message>
+    <message>
+        <location line="-143"/>
+        <source>Password</source>
+        <translation>Kata sandi</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please choose a secure password. The exact requirements for password strength may depend on your server.</source>
+        <translation>Mohon memilih kata sandi yang aman. Persyaratan untuk kekuatan sandi mungkin bergantung pada server Anda.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Password confirmation</source>
+        <translation>Konfirmasi kata sandi</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Homeserver</source>
+        <translation>Homeserver</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
+        <translation>Sebuah server yang mengizinkan pendaftaran. Karena Matrix itu terdecentralisasi, Anda pertama harus mencari server yang Anda daftar atau host server Anda sendiri.</translation>
+    </message>
+    <message>
+        <location line="+35"/>
+        <source>REGISTER</source>
+        <translation>DAFTAR</translation>
+    </message>
+    <message>
+        <location line="+169"/>
+        <source>Autodiscovery failed. Received malformed response.</source>
+        <translation>Penemuan otomatis gagal. Menerima respons cacat.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
+        <translation>Penemuan otomatis gagal. Terjadi kesalahan yang tidak diketahui saat meminta .well-known.</translation>
+    </message>
+    <message>
+        <location line="+24"/>
+        <source>The required endpoints were not found. Possibly not a Matrix server.</source>
+        <translation>Titik akhir yang dibutuhkan tidak dapat ditemukan. Kemungkinan bukan server Matrix.</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Received malformed response. Make sure the homeserver domain is valid.</source>
+        <translation>Menerima respons cacat. Pastikan domain homeservernya valid.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
+        <translation>Terjadi kesalahan yang tidak diketahui. Pastikan domain homeservernya valid.</translation>
+    </message>
+    <message>
+        <location line="-107"/>
+        <source>Password is not long enough (min 8 chars)</source>
+        <translation>Kata sandi kurang panjang (min. 8 karakter)</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Passwords don&apos;t match</source>
+        <translation>Kata sandi tidak cocok</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Invalid server name</source>
+        <translation>Nama server tidak valid</translation>
+    </message>
+</context>
+<context>
+    <name>ReplyPopup</name>
+    <message>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
+        <source>Close</source>
+        <translation>Tutup</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Cancel edit</source>
+        <translation>Batalkan pengeditan</translation>
+    </message>
+</context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Temukan Ruangan Publik</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Cari ruangan publik</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>RoomInfo</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation>tidak ada versi yang disimpan</translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
+        <translation>Tanda baru</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Enter the tag you want to use:</source>
+        <translation>Masukkan tanda yang Anda ingin gunakan:</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Leave room</source>
+        <translation>Tinggalkan ruangan</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Tag room as:</source>
+        <translation>Tandai ruangan sebagai:</translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Favourite</source>
+        <translation>Favorit</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Low priority</source>
+        <translation>Prioritas rendah</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Server notice</source>
+        <translation>Pemberitahuan server</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Create new tag...</source>
+        <translation>Membuat tanda baru…</translation>
+    </message>
+    <message>
+        <location line="+278"/>
+        <source>Status Message</source>
+        <translation>Pesan Status</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Enter your status message:</source>
+        <translation>Masukkan pesan status Anda:</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Profile settings</source>
+        <translation>Pengaturan profil</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Set status message</source>
+        <translation>Tetapkan pesan status</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Logout</source>
+        <translation>Keluar</translation>
+    </message>
+    <message>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Tutup</translation>
+    </message>
+    <message>
+        <location line="+65"/>
+        <source>Start a new chat</source>
+        <translation>Mulai chat baru</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Join a room</source>
+        <translation>Bergabung sebuah ruangan</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Create a new room</source>
+        <translation>Buat ruangan baru</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Room directory</source>
+        <translation>Direktori ruangan</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>User settings</source>
+        <translation>Pengaturan pengguna</translation>
+    </message>
+</context>
+<context>
+    <name>RoomMembers</name>
+    <message>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
+        <source>Members of %1</source>
+        <translation>Anggota dari %1</translation>
+    </message>
+    <message numerus="yes">
+        <location line="+33"/>
+        <source>%n people in %1</source>
+        <comment>Summary above list of members</comment>
+        <translation>
+            <numerusform>%n orang di %1</numerusform>
+        </translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Invite more people</source>
+        <translation>Undang banyak orang</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ruangan ini tidak terenkripsi!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Pengguna ini sudah diverifikasi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Pengguna ini belum diverifikasi, tetapi masih menggunakan kunci utama dari pertama kali Anda bertemu.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Pengguna ini memiliki perangkat yang belum diverifikasi!</translation>
+    </message>
+</context>
+<context>
+    <name>RoomSettings</name>
+    <message>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
+        <source>Room Settings</source>
+        <translation>Pengaturan Ruangan</translation>
+    </message>
+    <message>
+        <location line="+81"/>
+        <source>%1 member(s)</source>
+        <translation>%1 anggota</translation>
+    </message>
+    <message>
+        <location line="+55"/>
+        <source>SETTINGS</source>
+        <translation>PENGATURAN</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Notifications</source>
+        <translation>Notifikasi</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Muted</source>
+        <translation>Bisukan</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Mentions only</source>
+        <translation>Sebutan saja</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>All messages</source>
+        <translation>Semua pesan</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Akses ruangan</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Anyone and guests</source>
+        <translation>Siapa saja dan tamu</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Anyone</source>
+        <translation>Siapa saja</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Invited users</source>
+        <translation>Pengguna yang diundang</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>Dengan mengetuk</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Dibatasi oleh keanggotaan di ruangan lain</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Encryption</source>
+        <translation>Enkripsi</translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>End-to-End Encryption</source>
+        <translation>Enkripsi Ujung-ke-Ujung</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
+                            Please take note that it can&apos;t be disabled afterwards.</source>
+        <translation>Enkripsi saat ini masih eksperimental dan apapun dapat rusak secara tiba-tiba.&lt;br&gt;Mohon dicatat bahwa enkripsi tidak dapat dinonaktifkan setelah ini.</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Sticker &amp; Emote Settings</source>
+        <translation>Pengaturan Stiker &amp; Emote</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Change</source>
+        <translation>Ubah</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Change what packs are enabled, remove packs or create new ones</source>
+        <translation>Ubah paket apa yang diaktifkan, hapus paket atau buat yang baru</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>INFO</source>
+        <translation>INFO</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Internal ID</source>
+        <translation>ID Internal</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Room Version</source>
+        <translation>Versi Ruangan</translation>
+    </message>
+    <message>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
+        <source>Failed to enable encryption: %1</source>
+        <translation>Gagal mengaktifkan enkripsi: %1</translation>
+    </message>
+    <message>
+        <location line="+249"/>
+        <source>Select an avatar</source>
+        <translation>Pilih sebuah avatar</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>All Files (*)</source>
+        <translation>Semua File (*)</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>The selected file is not an image</source>
+        <translation>File yang dipilih bukan sebuah gambar</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Error while reading file: %1</source>
+        <translation>Terjadi kesalahan saat membaca file: %1</translation>
+    </message>
+    <message>
+        <location line="+32"/>
+        <location line="+19"/>
+        <source>Failed to upload image: %s</source>
+        <translation>Gagal mengunggah gambar: %s</translation>
+    </message>
+</context>
+<context>
+    <name>RoomlistModel</name>
+    <message>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
+        <source>Pending invite.</source>
+        <translation>Undangan tertunda.</translation>
+    </message>
+    <message>
+        <location line="+35"/>
+        <source>Previewing this room</source>
+        <translation>Menampilkan ruangan ini</translation>
+    </message>
+    <message>
+        <location line="+38"/>
+        <source>No preview available</source>
+        <translation>Tidak ada tampilan yang tersedia</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ScreenShare</name>
+    <message>
+        <location filename="../qml/voip/ScreenShare.qml" line="+30"/>
+        <source>Share desktop with %1?</source>
+        <translation>Bagikan desktop dengan %1?</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Window:</source>
+        <translation>Jendela:</translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Frame rate:</source>
+        <translation>Frame rate:</translation>
+    </message>
+    <message>
+        <location line="+19"/>
+        <source>Include your camera picture-in-picture</source>
+        <translation>Tambahkan kamera Anda dalam picture-in-picture</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Request remote camera</source>
+        <translation>Minta kamera jarak jauh</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <location line="+9"/>
+        <source>View your callee&apos;s camera like a regular video call</source>
+        <translation>Tampilkan kamera pengguna yang menerima panggilan seperti panggilan video biasa</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Hide mouse cursor</source>
+        <translation>Sembunyikan kursor mouse</translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Share</source>
+        <translation>Bagikan</translation>
+    </message>
+    <message>
+        <location line="+19"/>
+        <source>Preview</source>
+        <translation>Tampilkan</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+</context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Gagal menghubungkan ke penyimpanan rahasia</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Gagal memperbarui paket gambar: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Gagal menghapus paket gambar yang lama: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Gagal membuka gambar: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Gagal mengunggah gambar: %1</translation>
+    </message>
+</context>
+<context>
+    <name>StatusIndicator</name>
+    <message>
+        <location filename="../qml/StatusIndicator.qml" line="+24"/>
+        <source>Failed</source>
+        <translation>Gagal</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Sent</source>
+        <translation>Terkirim</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Received</source>
+        <translation>Diterima</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Read</source>
+        <translation>Dibaca</translation>
+    </message>
+</context>
+<context>
+    <name>StickerPicker</name>
+    <message>
+        <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
+        <source>Search</source>
+        <translation>Cari</translation>
+    </message>
+</context>
+<context>
+    <name>Success</name>
+    <message>
+        <location filename="../qml/device-verification/Success.qml" line="+11"/>
+        <source>Successful Verification</source>
+        <translation>Verifikasi Berhasil</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Verification successful! Both sides verified their devices!</source>
+        <translation>Verifikasi berhasil!  Kedua sisi telah memverifikasi perangkat mereka!</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Close</source>
+        <translation>Tutup</translation>
+    </message>
+</context>
+<context>
+    <name>TimelineModel</name>
+    <message>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
+        <source>Message redaction failed: %1</source>
+        <translation>Reaksi pesan gagal: %1</translation>
+    </message>
+    <message>
+        <location line="+71"/>
+        <location line="+5"/>
+        <source>Failed to encrypt event, sending aborted!</source>
+        <translation>Gagal mendekripsikan peristiwa, pengiriman dihentikan!</translation>
+    </message>
+    <message>
+        <location line="+169"/>
+        <source>Save image</source>
+        <translation>Simpan gambar</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save video</source>
+        <translation>Simpan video</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save audio</source>
+        <translation>Simpan audio</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Save file</source>
+        <translation>Simpan file</translation>
+    </message>
+    <message numerus="yes">
+        <location line="+251"/>
+        <source>%1 and %2 are typing.</source>
+        <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
+        <translation>
+            <numerusform>%1 dan %2 sedang mengetik.</numerusform>
+        </translation>
+    </message>
+    <message>
+        <location line="+66"/>
+        <source>%1 opened the room to the public.</source>
+        <translation>%1 membuka ruangan ke publik.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 made this room require and invitation to join.</source>
+        <translation>%1 membuat ruangan ini membutuhkan undangan untuk bergabung.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 mengizinkan siapa saja untuk bergabung ke ruangan ini dengan mengetuk.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 mengizinkan anggota dari ruangan berikut untuk bergabung ke ruangan ini secara otomatis: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>%1 made the room open to guests.</source>
+        <translation>%1 membuat ruangan ini terbuka ke tamu.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 has closed the room to guest access.</source>
+        <translation>%1 telah menutup ruangan ke akses tamu.</translation>
+    </message>
+    <message>
+        <location line="+23"/>
+        <source>%1 made the room history world readable. Events may be now read by non-joined people.</source>
+        <translation>%1 membuat sejarah ruangan dibaca oleh siapa saja. Peristiwa mungkin bisa dibaca oleh orang yang tidak bergabung.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>%1 set the room history visible to members from this point on.</source>
+        <translation>%1 membuat sejarah ruangan bisa dilihat oleh anggota dari titik sekarang.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 set the room history visible to members since they were invited.</source>
+        <translation>%1 membuat sejarah ruangan bisa dilihat oleh anggota yang telah diundang.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 set the room history visible to members since they joined the room.</source>
+        <translation>%1 membuat sejarah ruangan bisa dilihat oleh anggota yang telah bergabung ke ruangan ini.</translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>%1 has changed the room&apos;s permissions.</source>
+        <translation>%1 telah mengubah izin ruangan.</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>%1 was invited.</source>
+        <translation>%1 diundang.</translation>
+    </message>
+    <message>
+        <location line="+18"/>
+        <source>%1 changed their avatar.</source>
+        <translation>%1 mengubah avatarnya.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 changed some profile info.</source>
+        <translation>%1 mengubah info profil.</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>%1 joined.</source>
+        <translation>%1 bergabung.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 bergabung via otorisasi dari servernya %2.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>%1 rejected their invite.</source>
+        <translation>%1 menolak undangannya.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Revoked the invite to %1.</source>
+        <translation>Menghapus undangan ke %1.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 left the room.</source>
+        <translation>%1 meninggalkan ruangan.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Kicked %1.</source>
+        <translation>%1 dikeluarkan.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unbanned %1.</source>
+        <translation>Cekalan %1 dihilangkan.</translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>%1 was banned.</source>
+        <translation>%1 telah dicekal.</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Reason: %1</source>
+        <translation>Alasan: %1</translation>
+    </message>
+    <message>
+        <location line="-19"/>
+        <source>%1 redacted their knock.</source>
+        <translation>%1 menolak ketukannya.</translation>
+    </message>
+    <message>
+        <location line="-951"/>
+        <source>You joined this room.</source>
+        <translation>Anda bergabung ruangan ini.</translation>
+    </message>
+    <message>
+        <location line="+912"/>
+        <source>%1 has changed their avatar and changed their display name to %2.</source>
+        <translation>%1 mengubah avatarnya dan ubah nama tampilannya ke %2.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>%1 has changed their display name to %2.</source>
+        <translation>%1 mengubah nama tampilannya ke %2.</translation>
+    </message>
+    <message>
+        <location line="+37"/>
+        <source>Rejected the knock from %1.</source>
+        <translation>Menolak ketukan dari %1.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 left after having already left!</source>
+        <comment>This is a leave event after the user already left and shouldn&apos;t happen apart from state resets</comment>
+        <translation>%1 keluar setelah sudah keluar!</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>%1 knocked.</source>
+        <translation>%1 mengetuk.</translation>
+    </message>
+</context>
+<context>
+    <name>TimelineRow</name>
+    <message>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
+        <source>Edited</source>
+        <translation>Diedit</translation>
+    </message>
+</context>
+<context>
+    <name>TimelineView</name>
+    <message>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
+        <source>No room open</source>
+        <translation>Tidak ada ruangan yang dibuka</translation>
+    </message>
+    <message>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Tidak ada tampilan yang tersedia</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 member(s)</source>
+        <translation>%1 anggota</translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>join the conversation</source>
+        <translation>bergabung ke percakapan</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>accept invite</source>
+        <translation>terima undangan</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>decline invite</source>
+        <translation>tolak undangan</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Back to room list</source>
+        <translation>Kembali ke daftar ruangan</translation>
+    </message>
+</context>
+<context>
+    <name>TopBar</name>
+    <message>
+        <location filename="../qml/TopBar.qml" line="+59"/>
+        <source>Back to room list</source>
+        <translation>Kembali ke daftar ruangan</translation>
+    </message>
+    <message>
+        <location line="-44"/>
+        <source>No room selected</source>
+        <translation>Tidak ada ruangan yang dipilih</translation>
+    </message>
+    <message>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ruangan ini tidak dienkripsi!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Ruangan ini hanya berisi perangkat yang telah diverifikasi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Ruangan ini berisi perangkat yang belum diverifikasi!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>Room options</source>
+        <translation>Opsi ruangan</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Invite users</source>
+        <translation>Undang pengguna</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Members</source>
+        <translation>Anggota</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Leave room</source>
+        <translation>Tinggalkan ruangan</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Settings</source>
+        <translation>Pengaturan</translation>
+    </message>
+</context>
+<context>
+    <name>TrayIcon</name>
+    <message>
+        <location filename="../../src/TrayIcon.cpp" line="+112"/>
+        <source>Show</source>
+        <translation>Tampilkan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Quit</source>
+        <translation>Tutup</translation>
+    </message>
+</context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Mohon masukkan token pendaftaran yang valid.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>UserProfile</name>
+    <message>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
+        <source>Global User Profile</source>
+        <translation>Profil Pengguna Global</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Room User Profile</source>
+        <translation>Profil Pengguna di Ruangan</translation>
+    </message>
+    <message>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Ubah avatar secara global.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Ubah avatar. Hanya diterapkan di ruangan ini.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Ubah nama tampilan secara global.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Ubah nama tampilan. Hanya diterapkan di ruangan ini.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Ruangan: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>Ini adalah profile specifik ruangan. Nama pengguna dan avatar mungkin berbeda dari versi globalnya.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Buka profil global untuk pengguna ini.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation>Verifikasi</translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Mulai chat privat.</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Keluarkan pengguna ini.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Cekal pengguna ini.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Unverify</source>
+        <translation>Hilangkan verifikasi</translation>
+    </message>
+    <message>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
+        <source>Select an avatar</source>
+        <translation>Pilih sebuah avatar</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>All Files (*)</source>
+        <translation>Semua File (*)</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>The selected file is not an image</source>
+        <translation>File yang dipilih bukan sebuah gambar</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>Error while reading file: %1</source>
+        <translation>Terjadi kesalahan saat membaca file: %1</translation>
+    </message>
+</context>
+<context>
+    <name>UserSettings</name>
+    <message>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
+        <source>Default</source>
+        <translation>Default</translation>
+    </message>
+</context>
+<context>
+    <name>UserSettingsPage</name>
+    <message>
+        <location line="+567"/>
+        <source>Minimize to tray</source>
+        <translation>Perkecil ke baki</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Start in tray</source>
+        <translation>Mulai di baki</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Group&apos;s sidebar</source>
+        <translation>Bilah samping grup</translation>
+    </message>
+    <message>
+        <location line="-6"/>
+        <source>Circular Avatars</source>
+        <translation>Avatar Bundar</translation>
+    </message>
+    <message>
+        <location line="-217"/>
+        <source>profile: %1</source>
+        <translation>profil: %1</translation>
+    </message>
+    <message>
+        <location line="+104"/>
+        <source>Default</source>
+        <translation>Default</translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>CALLS</source>
+        <translation>PANGGILAN</translation>
+    </message>
+    <message>
+        <location line="+46"/>
+        <source>Cross Signing Keys</source>
+        <translation>Kunci Tanda Tangan Silang</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>REQUEST</source>
+        <translation>MINTA</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>DOWNLOAD</source>
+        <translation>UNDUH</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Keep the application running in the background after closing the client window.</source>
+        <translation>Membiarkan aplikasi berjalan di latar belakang setelah menutup jendela klien.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Start the application in the background without showing the client window.</source>
+        <translation>Mulai aplikasinya di latar belakang tanpa menunjukkan jendela kliennya.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Change the appearance of user avatars in chats.
+OFF - square, ON - Circle.</source>
+        <translation>Ubah penampilan avatar pengguna di chat.
+MATI - persegi, NYALA - Lingkaran.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Show a column containing groups and tags next to the room list.</source>
+        <translation>Menampilkan kolom yang berisi grup dan tanda di sebelah daftar ruangan.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Decrypt messages in sidebar</source>
+        <translation>Dekripsikan pesan di bilah samping</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Decrypt the messages shown in the sidebar.
+Only affects messages in encrypted chats.</source>
+        <translation>Dekripsi pesan yang ditampilkan di bilah samping.
+Hanya mempengaruhi pesan di chat terenkripsi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Privacy Screen</source>
+        <translation>Layar Privasi</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>When the window loses focus, the timeline will
+be blurred.</source>
+        <translation>Ketika jendela kehilangan fokus, linimasanya
+akan diburamkan.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Privacy screen timeout (in seconds [0 - 3600])</source>
+        <translation>Waktu kehabisan layar privasi (dalam detik [0 - 3600])</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Set timeout (in seconds) for how long after window loses
+focus before the screen will be blurred.
+Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds)</source>
+        <translation>Tetapkan waktu kehabisan (dalam detik) untuk berapa lama setelah jendela kehilangan
+fokus sebelum layarnya akan diburamkan.
+Tetapkan ke 0 untuk memburamkan secara langsung setelah kehilangan fokus. Nilai maksimum adalah 1 jam (3600 detik)</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Show buttons in timeline</source>
+        <translation>Tampilkan tombol di linimasa</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Show buttons to quickly reply, react or access additional options next to each message.</source>
+        <translation>Tampilkan tombol untuk membalas, merekasi, atau mengakses opsi tambahan di sebelah pesan dengan cepat.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Limit width of timeline</source>
+        <translation>Batasi lebar linimasa</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised</source>
+        <translation>Tetapkan lebar maksimum pesan di linimasa (dalam pixel). Ini bisa membantu keterbacaan di layar lebar, ketika Nheko dimaksimalkan.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Typing notifications</source>
+        <translation>Notifikasi mengetik</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Show who is typing in a room.
+This will also enable or disable sending typing notifications to others.</source>
+        <translation>Tampilkan siapa yang sedang mengetik dalam ruangan.
+Ini akan mengaktifkan atau menonaktifkan pengiriman notifikasi pengetikan ke yang lain.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Sort rooms by unreads</source>
+        <translation>Urutkan ruangan bedasarkan yang belum dibaca</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display rooms with new messages first.
+If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room.
+If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don&apos;t seem to consider them as important as the other rooms.</source>
+        <translation>Menampilkan ruangan dengan pesan baru pertama.
+Jika ini dimatikan, daftar ruangan hanya diurutkan dari waktu pesan terakhir di ruangan.
+Jika ini dinyalakan, ruangan yang mempunyai notifikasi aktif (lingkaran kecil dengan nomor didalam) akan diurutkan di atas. Ruangan, yang dibisukan, masih diurutkan dari waktu, karena Anda tidak pertimangkan mereka sebagai penting dengan ruangan yang lain.</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Read receipts</source>
+        <translation>Laporan dibaca</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Show if your message was read.
+Status is displayed next to timestamps.</source>
+        <translation>Menampilkan jika pesan Anda telah dibaca.
+Status akan ditampilkan disebelah waktu menerima pesan.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Send messages as Markdown</source>
+        <translation>Kirim pesan sebagai Markdown</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Allow using markdown in messages.
+When disabled, all messages are sent as a plain text.</source>
+        <translation>Memperbolehkan menggunakan Markdown di pesan.
+Ketika dinonaktifkan, semua pesan akan dikirim sebagai teks biasa.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Mainkan gambar beranimasi hanya saat kursor diarahkan ke gambar</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Desktop notifications</source>
+        <translation>Notifikasi desktop</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Notify about received message when the client is not currently focused.</source>
+        <translation>Memberitahukan tentang pesan yang diterima ketika kliennya tidak difokuskan.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Alert on notification</source>
+        <translation>Beritahu saat ada notifikasi</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Show an alert when a message is received.
+This usually causes the application icon in the task bar to animate in some fashion.</source>
+        <translation>Menampilkan pemberitahuan saat sebuah pesan diterima.
+Ini biasanya menyebabkan ikon aplikasi di bilah tugas untuk beranimasi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Highlight message on hover</source>
+        <translation>Highlight pesan saat kursor di atas pesan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Change the background color of messages when you hover over them.</source>
+        <translation>Mengubah warna background pesan ketika kursor Anda di atas pesannya.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Large Emoji in timeline</source>
+        <translation>Emoji besar di linimasa</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Make font size larger if messages with only a few emojis are displayed.</source>
+        <translation>Membuat ukuran font lebih besar jika pesan dengan beberapa emoji ditampilkan.</translation>
+    </message>
+    <message>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Kirim pesan terenkripsi ke pengguna yang telah diverifikasi saja</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Memerlukan pengguna diverifikasi untuk mengirim pesan terenkripsi ke pengguna. Ini meningkatkan keamanan tetapi membuat enkripsi ujung-ke-ujung lebih susah.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Share keys with verified users and devices</source>
+        <translation>Bagikan kunci dengan pengguna dan perangkat yang telah diverifikasi</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Secara otomatis membalas ke permintaan kunci dari pengguna lain, jika mereka terverifikasi, bahkan jika perangkat itu seharusnya tidak mempunyai akses ke kunci itu secara lain.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Cadangan Kunci Online</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Unduh kunci enkripsi pesan dari dan unggah ke cadangan kunci online terenkripsi.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Aktifkan cadangan kunci online</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>Pengembang Nheko merekomendasikan untuk tidak mengaktifkan pencadangan kunci online hingga pencadangan kunci online simetris tersedia. Tetap mengaktifkan?</translation>
+    </message>
+    <message>
+        <location line="+253"/>
+        <source>CACHED</source>
+        <translation>DICACHE</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>NOT CACHED</source>
+        <translation>TIDAK DICACHE</translation>
+    </message>
+    <message>
+        <location line="-495"/>
+        <source>Scale factor</source>
+        <translation>Faktor skala</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Change the scale factor of the whole user interface.</source>
+        <translation>Mengubah faktor skala antarmuka pengguna.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Font size</source>
+        <translation>Ukuran font</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Font Family</source>
+        <translation>Keluarga Font</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Theme</source>
+        <translation>Tema</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Ringtone</source>
+        <translation>Nada Dering</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Set the notification sound to play when a call invite arrives</source>
+        <translation>Tetapkan suara notifikasi untuk dimainkan ketika ada panggilan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Microphone</source>
+        <translation>Mikrofon</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Camera</source>
+        <translation>Kamera</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Camera resolution</source>
+        <translation>Resolusi kamera</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Camera frame rate</source>
+        <translation>Frame rate kamera</translation>
+    </message>
+    <message>
+        <location line="+14"/>
+        <source>Allow fallback call assist server</source>
+        <translation>Izinkan panggilan menggunakan bantuan server sebagai cadangan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Will use turn.matrix.org as assist when your home server does not offer one.</source>
+        <translation>Akan menggunakan turn.matrix.org sebagai bantuan jika homeserver Anda tidak menawarkannya.</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Device ID</source>
+        <translation>ID Perangkat</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Device Fingerprint</source>
+        <translation>Sidik Jari Perangkat</translation>
+    </message>
+    <message>
+        <location line="-166"/>
+        <source>Session Keys</source>
+        <translation>Kunci Perangkat</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>IMPORT</source>
+        <translation>IMPOR</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>EXPORT</source>
+        <translation>EKSPOR</translation>
+    </message>
+    <message>
+        <location line="-34"/>
+        <source>ENCRYPTION</source>
+        <translation>ENKRIPSI</translation>
+    </message>
+    <message>
+        <location line="-123"/>
+        <source>GENERAL</source>
+        <translation>UMUM</translation>
+    </message>
+    <message>
+        <location line="+72"/>
+        <source>INTERFACE</source>
+        <translation>ANTARMUKA</translation>
+    </message>
+    <message>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Memainkan media seperti GIF atau WEBP ketika kursor di atas medianya.</translation>
+    </message>
+    <message>
+        <location line="+17"/>
+        <source>Touchscreen mode</source>
+        <translation>Mode layar sentuh</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Will prevent text selection in the timeline to make touch scrolling easier.</source>
+        <translation>Akan mencegah seleksi teks di linimasi untuk membuat guliran mudah.</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Emoji Font Family</source>
+        <translation>Keluarga Font Emoji</translation>
+    </message>
+    <message>
+        <location line="+53"/>
+        <source>Master signing key</source>
+        <translation>Kunci penandatanganan utama</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Your most important key. You don&apos;t need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys.</source>
+        <translation>Kunci Anda yang paling penting. Anda tidak perlu menyimpannya dalam cache, karena tidak menyimpannya akan memperkecil kemungkinannya untuk dicuri dan hanya diperlukan untuk memutar kunci penandatanganan Anda yang lain.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>User signing key</source>
+        <translation>Kunci penandatanganan pengguna</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>The key to verify other users. If it is cached, verifying a user will verify all their devices.</source>
+        <translation>Kunci untuk memverifikasi pengguna lain. Jika dicache, memverifikasi sebuah pengguna akan memverifikasi semua perangkat mereka.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Self signing key</source>
+        <translation>Kunci penandatanganan diri</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you.</source>
+        <translation>Kunci untuk memverifikasi perangkat Anda. Jika dicache, memverifikasi salah satu perangkat Anda akan menandainya sebagai terverifikasi untuk semua perangkat Anda yang lain dan untuk pengguna yang telah memverifikasi Anda.</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Backup key</source>
+        <translation>Kunci cadangan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server.</source>
+        <translation>Kunci untuk mendekripsikan cadangan kunci online. Jika dicache, Anda dapat mengaktifkan kunci cadangan online untuk menyimpan kunci enkripsi yang dienkripsi secara aman di servernya.</translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Select a file</source>
+        <translation>Pilih sebuah file</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>All Files (*)</source>
+        <translation>Semua File (*)</translation>
+    </message>
+    <message>
+        <location line="+265"/>
+        <source>Open Sessions File</source>
+        <translation>Buka File Sesi</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <location line="+18"/>
+        <location line="+8"/>
+        <location line="+19"/>
+        <location line="+11"/>
+        <location line="+18"/>
+        <source>Error</source>
+        <translation>Kesalahan</translation>
+    </message>
+    <message>
+        <location line="-65"/>
+        <location line="+27"/>
+        <source>File Password</source>
+        <translation>Kata Sandi File</translation>
+    </message>
+    <message>
+        <location line="-26"/>
+        <source>Enter the passphrase to decrypt the file:</source>
+        <translation>Masukkan kata sandi untuk mendekripsi filenya:</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <location line="+27"/>
+        <source>The password cannot be empty</source>
+        <translation>Kata sandi tidak boleh kosong</translation>
+    </message>
+    <message>
+        <location line="-8"/>
+        <source>Enter passphrase to encrypt your session keys:</source>
+        <translation>Masukkan frasa sandi untuk mengenkripsikan kunci sesi Anda:</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>File to save the exported session keys</source>
+        <translation>File untuk menyimpan kunci sesi yang telah diekspor</translation>
+    </message>
+</context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Tidak ada chat privat terenkripsi ditemukan dengan pengguna ini. Buat chat privat terenkripsi dengan pengguna ini dan coba lagi.</translation>
+    </message>
+</context>
+<context>
+    <name>Waiting</name>
+    <message>
+        <location filename="../qml/device-verification/Waiting.qml" line="+12"/>
+        <source>Waiting for other party…</source>
+        <translation>Menunggu untuk mengguna lain…</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>Waiting for other side to accept the verification request.</source>
+        <translation>Menunggu untuk pengguna yang lain untuk menerima permintaan verifikasi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Waiting for other side to continue the verification process.</source>
+        <translation>Menunggu untuk pengguna lain untuk melanjutkan proses verifikasi.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Waiting for other side to complete the verification process.</source>
+        <translation>Menunggu untuk pengguna lain untuk menyelesaikan proses verifikasi.</translation>
+    </message>
+    <message>
+        <location line="+15"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+</context>
+<context>
+    <name>WelcomePage</name>
+    <message>
+        <location filename="../../src/WelcomePage.cpp" line="+34"/>
+        <source>Welcome to nheko! The desktop client for the Matrix protocol.</source>
+        <translation>Selamat datang di nheko! Sebuah klien desktop untuk protokol Matrix.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Enjoy your stay!</source>
+        <translation>Nikmati masa tinggal Anda di sini!</translation>
+    </message>
+    <message>
+        <location line="+23"/>
+        <source>REGISTER</source>
+        <translation>DAFTAR</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>LOGIN</source>
+        <translation>MASUK</translation>
+    </message>
+</context>
+<context>
+    <name>descriptiveTime</name>
+    <message>
+        <location filename="../../src/Utils.cpp" line="+184"/>
+        <source>Yesterday</source>
+        <translation>Kemarin</translation>
+    </message>
+</context>
+<context>
+    <name>dialogs::CreateRoom</name>
+    <message>
+        <location filename="../../src/dialogs/CreateRoom.cpp" line="+40"/>
+        <source>Create room</source>
+        <translation>Buat ruangan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Name</source>
+        <translation>Nama</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Topic</source>
+        <translation>Topik</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Alias</source>
+        <translation>Alias</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Room Visibility</source>
+        <translation>Visibilitas Ruangan</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Room Preset</source>
+        <translation>Preset Ruangan</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Direct Chat</source>
+        <translation>Chat Langsung</translation>
+    </message>
+</context>
+<context>
+    <name>dialogs::FallbackAuth</name>
+    <message>
+        <location filename="../../src/dialogs/FallbackAuth.cpp" line="+34"/>
+        <source>Open Fallback in Browser</source>
+        <translation>Buka Fallback di Peramban</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Confirm</source>
+        <translation>Konfirmasi</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>Open the fallback, follow the steps and confirm after completing them.</source>
+        <translation>Buka fallbacknya, ikuti petunjuknya dan konfirmasi setelah menyelesaikannya.</translation>
+    </message>
+</context>
+<context>
+    <name>dialogs::Logout</name>
+    <message>
+        <location filename="../../src/dialogs/Logout.cpp" line="+35"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Logout. Are you sure?</source>
+        <translation>Keluar. Apakah Anda yakin?</translation>
+    </message>
+</context>
+<context>
+    <name>dialogs::PreviewUploadOverlay</name>
+    <message>
+        <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+29"/>
+        <source>Upload</source>
+        <translation>Unggah</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+93"/>
+        <source>Media type: %1
+Media size: %2
+</source>
+        <translation>Tipe media: %1
+Ukuran media: %2
+</translation>
+    </message>
+</context>
+<context>
+    <name>dialogs::ReCaptcha</name>
+    <message>
+        <location filename="../../src/dialogs/ReCaptcha.cpp" line="+35"/>
+        <source>Cancel</source>
+        <translation>Batalkan</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Confirm</source>
+        <translation>Konfirmasi</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Solve the reCAPTCHA and press the confirm button</source>
+        <translation>Selesaikan reCAPTCHAnya dan tekan tombol konfirmasi</translation>
+    </message>
+</context>
+<context>
+    <name>message-description sent:</name>
+    <message>
+        <location filename="../../src/Utils.h" line="+115"/>
+        <source>You sent an audio clip</source>
+        <translation>Anda mengirim klip audio</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an audio clip</source>
+        <translation>%1 mengirim klip audio</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You sent an image</source>
+        <translation>Anda mengirim sebuah pesan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 sent an image</source>
+        <translation>%1 mengirim sebuah gambar</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You sent a file</source>
+        <translation>Anda mengirim sebuah file</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 sent a file</source>
+        <translation>%1 mengirim sebuah file</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You sent a video</source>
+        <translation>Anda mengirim sebuah video</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 sent a video</source>
+        <translation>%1 mengirim sebuah video</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You sent a sticker</source>
+        <translation>Anda mengirim sebuah stiker</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 sent a sticker</source>
+        <translation>%1 mengirim sebuah stiker</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You sent a notification</source>
+        <translation>Anda mengirim sebuah notifikasi</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent a notification</source>
+        <translation>%1 mengirim sebuah notifikasi</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You: %1</source>
+        <translation>Anda: %1</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1: %2</source>
+        <translation>%1: %2</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>You sent an encrypted message</source>
+        <translation>Anda mengirim sebuah pesan terenkripsi</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>%1 sent an encrypted message</source>
+        <translation>%1 mengirim sebuah pesan terenkripsi</translation>
+    </message>
+    <message>
+        <location line="+5"/>
+        <source>You placed a call</source>
+        <translation>Anda melakukan panggilan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 placed a call</source>
+        <translation>%1 melakukan panggilan</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You answered a call</source>
+        <translation>Anda menjawab panggilan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 answered a call</source>
+        <translation>%1 menjawab panggilan</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>You ended a call</source>
+        <translation>Anda mengakhiri panggilan</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>%1 ended a call</source>
+        <translation>%1 mengakhiri panggilan</translation>
+    </message>
+</context>
+<context>
+    <name>utils</name>
+    <message>
+        <location line="+3"/>
+        <source>Unknown Message Type</source>
+        <translation>Tipe Pesan Tidak Dikenal</translation>
+    </message>
+</context>
+</TS>
diff --git a/resources/langs/nheko_it.ts b/resources/langs/nheko_it.ts
index 5a1d45f891b630d78a9af46236190c5a93f4d5d5..17e466c861813c377cd3a4a94cd7591f5c9bc60f 100644
--- a/resources/langs/nheko_it.ts
+++ b/resources/langs/nheko_it.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Chiamata in corso...</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Chiamata video</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Chiamata Video</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Schermo completo</translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Impossibile invitare l&apos;utente: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Invitato utente: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Migrazione della cache alla versione corrente fallita. Questo può avere diverse cause. Per favore apri una issue e nel frattempo prova ad usare una versione più vecchia. In alternativa puoi provare a cancellare la cache manualmente.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Conferma collegamento</translation>
     </message>
@@ -151,23 +151,23 @@
         <translation>Vuoi davvero collegarti a %1?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Stanza %1 creata.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Conferma Invito</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Vuoi davvero inviare %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Impossibile invitare %1 a %2: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Vuoi davvero allontanare %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Scacciato utente: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation>Vuoi veramente bannare %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Impossibile bannare %1 in %2: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation>Vuoi veramente reintegrare %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Impossibile rimuovere il ban di %1 in %2: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Rimosso il ban dall&apos;utente: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation>Sei sicuro di voler avviare una chat privata con %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Migrazione della cache fallita!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>La cache sul tuo disco è più nuova di quella supportata da questa versione di Nheko. Per favore aggiorna o pulisci la tua cache.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Impossibile ripristinare l&apos;account OLM. Per favore accedi nuovamente.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Impossibile ripristinare i dati salvati. Per favore accedi nuovamente.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Impossibile configurare le chiavi crittografiche. Risposta del server: %1 %2. Per favore riprova in seguito.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Per favore prova ad accedere nuovamente: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Impossibile accedere alla stanza: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Sei entrato nella stanza</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Impossibile rimuovere l&apos;invito: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Creazione della stanza fallita: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Impossibile lasciare la stanza: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation>Fallita l&apos;espulsione di %1 da %2: %3</translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Decifra i segreti</translation>
     </message>
@@ -362,12 +364,12 @@
         <translation>Inserisci la chiave di recupero o la parola chiave per decriptare i tuoi segreti:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation>Inserisci la tua chiave di recupero o la parola chiave chiamata %1 per decifrare i tuoi segreti:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Decrittazione fallita</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Cerca</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Membri</translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Questo messaggio non è crittato!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation>Criptato da un dispositivo verificato</translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation>Criptato da un dispositivo non verificato ma hai già verificato questo utente.</translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation>Criptato da un dispositivo non verificato</translation>
+        <source>There was an error decrypting this message.</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>-- Evento Criptato (Nessuna chiave privata per la decriptazione) --</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation>-- Evento Criptato (Chiave non valida per questo indice) --</translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>-- Errore di Decrittazione (impossibile recuperare le chiavi megolm dal DB) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Errore di Decrittazione (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>-- Evento Criptato (Tipo di evento ignoto) --</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Questo messaggio non è crittato!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Criptato da un dispositivo verificato</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Criptato da un dispositivo non verificato ma hai già verificato questo utente.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Tempo di verifica del dispositivo scaduto.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>L&apos;altra parte ha annullato la verifica.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Chiudi</translation>
     </message>
@@ -604,6 +613,81 @@
         <translation>Inoltra Messaggio</translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">Annulla</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished">Modifica</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished">Chiudi</translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished">Seleziona un file</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished">Tutti i File (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished">Impossibile inviare il file multimediale. Per favore riprova.</translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">Annulla</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">ID della stanza o alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Lascia la stanza</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Sei sicuro di voler uscire?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -760,25 +885,25 @@ Esempio: https://server.mio:8787</translation>
         <translation>ACCEDI</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation>Hai inserito un ID Matrix non valido, es @joe:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Ricerca automatica fallita. Ricevuta risposta malformata.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Ricerca automatica fallita. Errore ignoto durante la richiesta di .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Gli endpoint richiesti non sono stati trovati. Forse non è un server Matrix.</translation>
     </message>
@@ -788,30 +913,48 @@ Esempio: https://server.mio:8787</translation>
         <translation>Ricevuta risposta malformata. Assicurati che il dominio dell&apos;homeserver sia valido.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Avvenuto un errore sconosciuto. Assicurati che il dominio dell&apos;homeserver sia valido.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>ACCESSO SSO</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Password vuota</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>Accesso SSO fallito</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>rimosso</translation>
@@ -822,7 +965,7 @@ Esempio: https://server.mio:8787</translation>
         <translation>Crittografia abilitata</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>nome della stanza cambiato in: %1</translation>
     </message>
@@ -881,6 +1024,11 @@ Esempio: https://server.mio:8787</translation>
         <source>Negotiating call...</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -905,7 +1053,7 @@ Esempio: https://server.mio:8787</translation>
         <translation type="unfinished">Scrivi un messaggio…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -928,7 +1076,7 @@ Esempio: https://server.mio:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Modifica</translation>
     </message>
@@ -948,17 +1096,19 @@ Esempio: https://server.mio:8787</translation>
         <translation type="unfinished">Opzioni</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1017,6 +1167,11 @@ Esempio: https://server.mio:8787</translation>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Esempio: https://server.mio:8787</translation>
         <translation>Richiesta di verifica ricevuta</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation>Per permettere agli altri utenti di vedere che dispositivi ti appartengono, puoi verificarli. Questo inoltre permette alle chiavi di recupero di funzionare automaticamente.
 Verificare %1 adesso?</translation>
@@ -1077,33 +1237,29 @@ Verificare %1 adesso?</translation>
         <translation>Accetta</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation type="unfinished">%1 ha inviato un messaggio criptato</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation>* %1 %2</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation>Risposta di %1: %2</translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished">%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1134,7 +1290,7 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished">Nessun microfono trovato.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1165,7 +1321,7 @@ Verificare %1 adesso?</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1180,21 +1336,37 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Ricevute di lettura</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Nome utente</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Il nome utente non deve essere vuoto e deve contenere solo i caratteri a-z, 0-9, ., _, =, -, e /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Password</translation>
     </message>
@@ -1214,7 +1386,7 @@ Verificare %1 adesso?</translation>
         <translation>Homeserver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Un server che consente la registrazione. Siccome matrix è decentralizzata, devi prima trovare un server su cui registrarti o ospitarne uno tuo.</translation>
     </message>
@@ -1224,27 +1396,17 @@ Verificare %1 adesso?</translation>
         <translation>REGISTRATI</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Non ci sono processi di registrazione supportati!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished">Ricerca automatica fallita. Ricevuta risposta malformata.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished">Ricerca automatica fallita. Errore ignoto durante la richiesta di .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished">Gli endpoint richiesti non sono stati trovati. Forse non è un server Matrix.</translation>
     </message>
@@ -1259,17 +1421,17 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished">Avvenuto un errore sconosciuto. Assicurati che il dominio dell&apos;homeserver sia valido.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>La password non è abbastanza lunga (minimo 8 caratteri)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Le password non corrispondono</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Nome del server non valido</translation>
     </message>
@@ -1277,7 +1439,7 @@ Verificare %1 adesso?</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Chiudi</translation>
     </message>
@@ -1288,33 +1450,41 @@ Verificare %1 adesso?</translation>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
-        <translation>nessuna versione memorizzata</translation>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>RoomInfo</name>
     <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation>nessuna versione memorizzata</translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1348,7 +1518,7 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1368,12 +1538,35 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished">Disconnettiti</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Chiudi</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished">Inizia una nuova discussione</translation>
     </message>
@@ -1393,7 +1586,7 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished">Elenco delle stanze</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished">Impostazioni utente</translation>
     </message>
@@ -1401,12 +1594,12 @@ Verificare %1 adesso?</translation>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1419,16 +1612,36 @@ Verificare %1 adesso?</translation>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1458,7 +1671,12 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1473,7 +1691,17 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1519,12 +1747,12 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished">Impossibile abilitare la crittografia: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished">Scegli un avatar</translation>
     </message>
@@ -1544,8 +1772,8 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished">Errore durante la lettura del file: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished">Impossibile fare l&apos;upload dell&apos;immagine: %s</translation>
     </message>
@@ -1553,21 +1781,49 @@ Verificare %1 adesso?</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1622,6 +1878,121 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished">Annulla</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1674,18 +2045,18 @@ Verificare %1 adesso?</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Oscuramento del messaggio fallito: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Salva immagine</translation>
     </message>
@@ -1705,7 +2076,7 @@ Verificare %1 adesso?</translation>
         <translation>Salva file</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1714,7 +2085,7 @@ Verificare %1 adesso?</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 ha aperto la stanza al pubblico.</translation>
     </message>
@@ -1724,7 +2095,17 @@ Verificare %1 adesso?</translation>
         <translation>%1 ha configurato questa stanza per richiedere un invito per entrare.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 ha configurato questa stanza affinché sia aperta ai visitatori.</translation>
     </message>
@@ -1744,12 +2125,12 @@ Verificare %1 adesso?</translation>
         <translation>%1 ha reso la cronologia della stanza visibile ai membri da questo momento in poi.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 ha reso la cronologia della stanza visibile ai membri dal momento in cui sono stati invitati.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 ha reso la cronologia della stanza visibile ai membri dal momento in cui sono entrati nella stanza.</translation>
     </message>
@@ -1759,12 +2140,12 @@ Verificare %1 adesso?</translation>
         <translation>%1 ha cambiato i permessi della stanza.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 è stato invitato.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 ha cambiato il suo avatar.</translation>
     </message>
@@ -1774,12 +2155,17 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 è entrato.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 ha rifiutato il suo invito.</translation>
     </message>
@@ -1809,32 +2195,32 @@ Verificare %1 adesso?</translation>
         <translation>%1 è stato bannato.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 ha oscurato la sua bussata.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Sei entrato in questa stanza.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Rifiutata la bussata di %1.</translation>
     </message>
@@ -1853,7 +2239,7 @@ Verificare %1 adesso?</translation>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1861,12 +2247,17 @@ Verificare %1 adesso?</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Nessuna stanza aperta</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1891,28 +2282,40 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished">Opzioni della stanza</translation>
     </message>
@@ -1950,10 +2353,35 @@ Verificare %1 adesso?</translation>
         <translation>Esci</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1963,33 +2391,98 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished">Scegli un avatar</translation>
     </message>
@@ -2012,8 +2505,8 @@ Verificare %1 adesso?</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2021,7 +2514,7 @@ Verificare %1 adesso?</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Minimizza nella tray</translation>
     </message>
@@ -2031,22 +2524,22 @@ Verificare %1 adesso?</translation>
         <translation>Avvia nella tray</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Barra laterale dei gruppi</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Avatar Circolari</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2071,7 +2564,7 @@ Verificare %1 adesso?</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2086,6 +2579,16 @@ Verificare %1 adesso?</translation>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2114,7 +2617,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2169,7 +2672,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Ricevute di lettura</translation>
     </message>
@@ -2180,7 +2683,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Invia messaggi come Markdown</translation>
     </message>
@@ -2192,6 +2695,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Notifiche desktop</translation>
     </message>
@@ -2232,12 +2740,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2247,7 +2790,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Fattore di scala</translation>
     </message>
@@ -2322,7 +2865,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Impronta digitale del dispositivo</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Chiavi di Sessione</translation>
     </message>
@@ -2342,17 +2885,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>CRITTOGRAFIA</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>GENERALE</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>INTERFACCIA</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2367,12 +2915,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Famiglia dei caratteri delle Emoji</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2392,7 +2935,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2422,14 +2965,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished">Tutti i File (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Apri File delle Sessioni</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2437,19 +2980,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Errore</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Password del File</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Inserisci la passphrase per decriptare il file:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>La password non può essere vuota</translation>
     </message>
@@ -2464,6 +3007,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>File ove salvare le chiavi di sessione esportate</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2589,37 +3140,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Apri il ripiego, segui i passaggi e conferma dopo averli completati.</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Entra</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Annulla</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>ID della stanza o alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Annulla</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Sei sicuro di voler uscire?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2673,32 +3193,6 @@ Peso media: %2
         <translation>Risolvi il reCAPTCHA e premi il pulsante di conferma</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Ricevute di lettura</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Chiudi</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Oggi %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Ieri %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2712,47 +3206,47 @@ Peso media: %2
         <translation>%1 ha inviato una clip audio</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Hai inviato un&apos;immagine</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 ha inviato un&apos;immagine</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Hai inviato un file</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 ha inviato un file</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Hai inviato un video</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 ha inviato un video</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Hai inviato uno sticker</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 ha inviato uno sticker</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Hai inviato una notifica</translation>
     </message>
@@ -2767,7 +3261,7 @@ Peso media: %2
         <translation>Tu: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2787,27 +3281,27 @@ Peso media: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2815,7 +3309,7 @@ Peso media: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Tipo di Messaggio sconosciuto</translation>
     </message>
diff --git a/resources/langs/nheko_ja.ts b/resources/langs/nheko_ja.ts
index 1ec862b01a80c614a98f65a0ac60c2f190a95523..7ae33c581d2384e185faeb2868eb97dfe835cc7d 100644
--- a/resources/langs/nheko_ja.ts
+++ b/resources/langs/nheko_ja.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation type="unfinished"></translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation type="unfinished"></translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>ユーザーを招待できませんでした: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>招待されたユーザー: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>%2に%1を招待できませんでした: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>一時的に追放されたユーザー: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>%2で%1を永久追放できませんでした: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>%2で%1の永久追放を解除できませんでした: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>永久追放を解除されたユーザー: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation type="unfinished"></translation>
     </message>
@@ -247,33 +247,35 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>OLMアカウントを復元できませんでした。もう一度ログインして下さい。</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>セーブデータを復元できませんでした。もう一度ログインして下さい。</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>暗号化鍵を設定できませんでした。サーバーの応答: %1 %2. 後でやり直して下さい。</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>もう一度ログインしてみて下さい: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>部屋に参加できませんでした: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>部屋に参加しました</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>招待を削除できませんでした: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>部屋を作成できませんでした: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>部屋から出られませんでした: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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">-- 復号エラー (データベースからmegolm鍵を取得できませんでした) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation type="unfinished">-- 復号エラー (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-52"/>
-        <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>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished">閉じる</translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">キャンセル</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished">閉じる</translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished">ファイルを選択</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished">全てのファイル (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished">メディアをアップロードできませんでした。やり直して下さい。</translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">キャンセル</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">部屋のID又は別名</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">部屋を出る</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">本当に退出しますか?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -756,25 +881,25 @@ Example: https://server.my:8787</source>
         <translation>ログイン</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>自動検出できませんでした。不正な形式の応答を受信しました。</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>自動検出できませんでした。.well-known要求時の不明なエラー。</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>必要な端点が見つかりません。Matrixサーバーではないかもしれません。</translation>
     </message>
@@ -784,30 +909,48 @@ Example: https://server.my:8787</source>
         <translation>不正な形式の応答を受信しました。ホームサーバーのドメイン名が有効であるかを確認して下さい。</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>不明なエラーが発生しました。ホームサーバーのドメイン名が有効であるかを確認して下さい。</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>パスワードが入力されていません</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
@@ -818,7 +961,7 @@ Example: https://server.my:8787</source>
         <translation>暗号化が有効です</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>部屋名が変更されました: %1</translation>
     </message>
@@ -877,6 +1020,11 @@ Example: https://server.my:8787</source>
         <source>Negotiating call...</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -901,7 +1049,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">メッセージを書く...</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -924,7 +1072,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">オプション</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1072,33 +1232,29 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">容認</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation type="unfinished">%1が暗号化されたメッセージを送信しました</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished">%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">開封確認</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>ユーザー名</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>パスワード</translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,27 +1391,17 @@ Example: https://server.my:8787</source>
         <translation>登録</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished">自動検出できませんでした。不正な形式の応答を受信しました。</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished">自動検出できませんでした。.well-known要求時の不明なエラー。</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished">必要な端点が見つかりません。Matrixサーバーではないかもしれません。</translation>
     </message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">不明なエラーが発生しました。ホームサーバーのドメイン名が有効であるかを確認して下さい。</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>パスワード長が不足しています (最小8文字)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>パスワードが一致しません</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>無効なサーバー名です</translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished">閉じる</translation>
     </message>
@@ -1283,33 +1445,41 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
-        <translation>バージョンが保存されていません</translation>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>RoomInfo</name>
     <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation>バージョンが保存されていません</translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished">ログアウト</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">閉じる</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished">新しいチャットを開始</translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">部屋一覧</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished">ユーザー設定</translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1413,16 +1606,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1452,7 +1665,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1467,7 +1685,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1513,12 +1741,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished">暗号化を有効にできませんでした: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished">アバターを選択</translation>
     </message>
@@ -1538,8 +1766,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">ファイルの読み込み時にエラーが発生しました: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished">画像をアップロードできませんでした: %s</translation>
     </message>
@@ -1547,21 +1775,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1616,6 +1872,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">キャンセル</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1668,18 +2039,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>メッセージを編集できませんでした: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>画像を保存</translation>
     </message>
@@ -1699,7 +2070,7 @@ Example: https://server.my:8787</source>
         <translation>ファイルを保存</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1707,7 +2078,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1717,7 +2088,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1737,12 +2118,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1752,12 +2133,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1が招待されました。</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1がアバターを変更しました。</translation>
     </message>
@@ -1767,12 +2148,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1が参加しました。</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1が招待を拒否しました。</translation>
     </message>
@@ -1802,32 +2188,32 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1がノックを編集しました。</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>%1からのノックを拒否しました。</translation>
     </message>
@@ -1846,7 +2232,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1854,12 +2240,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>部屋が開いていません</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1884,28 +2275,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished">部屋のオプション</translation>
     </message>
@@ -1943,10 +2346,35 @@ Example: https://server.my:8787</source>
         <translation>終了</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1956,33 +2384,98 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished">アバターを選択</translation>
     </message>
@@ -2005,8 +2498,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2014,7 +2507,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>トレイへ最小化</translation>
     </message>
@@ -2024,22 +2517,22 @@ Example: https://server.my:8787</source>
         <translation>トレイで起動</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>グループサイドバー</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>円形アバター</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2064,7 +2557,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2079,6 +2572,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2107,7 +2610,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2162,7 +2665,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>開封確認</translation>
     </message>
@@ -2173,7 +2676,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>メッセージをMarkdownとして送信</translation>
     </message>
@@ -2185,6 +2688,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>デスクトップ通知</translation>
     </message>
@@ -2225,12 +2733,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2240,7 +2783,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>尺度係数</translation>
     </message>
@@ -2315,7 +2858,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>デバイスの指紋</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>セッション鍵</translation>
     </message>
@@ -2335,17 +2878,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>暗号化</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>全般</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2360,12 +2908,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2385,7 +2928,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2415,14 +2958,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished">全てのファイル (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>セッションファイルを開く</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2430,19 +2973,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>エラー</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>ファイルのパスワード</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>ファイルを復号するためのパスフレーズを入力して下さい:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>パスワードを空にはできません</translation>
     </message>
@@ -2457,6 +3000,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>エクスポートされたセッション鍵を保存するファイル</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2582,37 +3133,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>参加</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>キャンセル</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>部屋のID又は別名</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>キャンセル</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>本当に退出しますか?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2666,32 +3186,6 @@ Media size: %2
         <translation>reCAPTCHAに解答して、確認ボタンを押して下さい</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>開封確認</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>閉じる</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>今日 %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>昨日 %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3199,47 @@ Media size: %2
         <translation>%1が音声データを送信しました</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>画像を送信しました</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1が画像を送信しました</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>ファイルを送信しました</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1がファイルを送信しました</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>動画を送信しました</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1が動画を送信しました</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>ステッカーを送信しました</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1がステッカーを送信しました</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>通知を送信しました</translation>
     </message>
@@ -2760,7 +3254,7 @@ Media size: %2
         <translation>あなた: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2780,27 +3274,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2808,7 +3302,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>不明なメッセージ型です</translation>
     </message>
diff --git a/resources/langs/nheko_ml.ts b/resources/langs/nheko_ml.ts
index 011107c2990447f3ef83a855c37895daa3549bbf..d8e25ff288dc491d1a5ed97800873103f4c8d3f3 100644
--- a/resources/langs/nheko_ml.ts
+++ b/resources/langs/nheko_ml.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>വിളിക്കുന്നു...</translation>
     </message>
@@ -17,7 +17,7 @@
     <message>
         <location line="+67"/>
         <source>You are screen sharing</source>
-        <translation type="unfinished"></translation>
+        <translation>നിങ്ങൾ സ്ക്രീൻ പങ്കിടുന്നു</translation>
     </message>
     <message>
         <location line="+17"/>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>വീഡിയോ കോൾ</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>വീഡിയോ കോൾ</translation>
     </message>
@@ -91,7 +91,7 @@
     <message>
         <location line="+10"/>
         <source>Accept</source>
-        <translation type="unfinished"></translation>
+        <translation>സ്വീകരിക്കുക</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -117,64 +117,64 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
-        <translation type="unfinished"></translation>
+        <translation>മുഴുവൻ സ്ക്രീൻ</translation>
     </message>
 </context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>ഉപയോക്താവിനെ ക്ഷണിക്കുന്നതിൽ പരാജയപ്പെട്ടു: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>ക്ഷണിച്ച ഉപയോക്താവ്:% 1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
-        <translation type="unfinished"></translation>
+        <translation>ചേരുന്നത് ഉറപ്പാക്കുക</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to join %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>നിങ്ങൾക്ക് %1 -ൽ ചേരാൻ ആഗ്രഹം ഉണ്ടോ?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>%1 മുറി സൃഷ്ടിച്ചു</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>ക്ഷണം ഉറപ്പാക്കു</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Confirm kick</source>
-        <translation type="unfinished"></translation>
+        <translation>പുറത്താക്കൽ ഉറപ്പാക്കുക</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -182,9 +182,9 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>ഉപയോക്താവിനെ പുറത്താക്കി: %1</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -227,12 +227,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation type="unfinished"></translation>
     </message>
@@ -247,33 +247,35 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>ദയവായി വീണ്ടും ലോഗിൻ ചെയ്യാൻ നോക്കുക: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>മുറിയിൽ ചേരുന്നതിൽ പരാജയം: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>നിങ്ങൾ മുറിയിൽ ചേർന്നു</translation>
     </message>
@@ -283,9 +285,9 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>മുറി സൃഷ്ടിക്കുന്നത് പരാജയപ്പെട്ടു: %1</translation>
     </message>
     <message>
         <location line="+18"/>
@@ -293,7 +295,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -321,7 +323,7 @@
     <message>
         <location line="+30"/>
         <source>Favourites</source>
-        <translation type="unfinished"></translation>
+        <translation>പ്രിയപ്പെട്ടവ</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -382,7 +384,7 @@
     <message>
         <location filename="../qml/device-verification/DigitVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>ഉറപ്പാക്കൽ കോഡ്</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -431,7 +433,7 @@
         <translation>തിരയുക</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>ആളുകൾ</translation>
     </message>
@@ -448,17 +450,17 @@
     <message>
         <location line="+2"/>
         <source>Activity</source>
-        <translation type="unfinished"></translation>
+        <translation>പ്രവർത്തനം</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Travel</source>
-        <translation type="unfinished"></translation>
+        <translation>യാത്ര</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Objects</source>
-        <translation type="unfinished"></translation>
+        <translation>സാധനങ്ങൾ</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -468,7 +470,7 @@
     <message>
         <location line="+2"/>
         <source>Flags</source>
-        <translation type="unfinished"></translation>
+        <translation>പതാകകൾ</translation>
     </message>
 </context>
 <context>
@@ -476,7 +478,7 @@
     <message>
         <location filename="../qml/device-verification/EmojiVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>ഉറപ്പാക്കൽ കോഡ്</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>കീ അഭ്യർ</translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-52"/>
-        <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>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -567,7 +567,7 @@
     <message>
         <location filename="../qml/device-verification/Failed.qml" line="+11"/>
         <source>Verification failed</source>
-        <translation type="unfinished"></translation>
+        <translation>ഉറപ്പാക്കൽ പരാജയപ്പെട്ടു</translation>
     </message>
     <message>
         <location line="+15"/>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>അടയ്‌ക്കുക</translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>ചിത്രങ്ങൾ ചേർക്കുക</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>സ്റ്റിക്കറുകൾ(*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>പാക്കിന്റെ പേര്</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>ഇമോജി ആയി ഉപയോഗിക്കുക</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>സ്റ്റിക്കറായി ഉപയോഗിക്കുക</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>നീക്കം ചെയ്യുക</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">റദ്ദാക്കു</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>സംരക്ഷിക്കുക</translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>അക്കൗണ്ട് പാക്ക് സൃഷ്ടിക്കുക</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>പുതിയ മുറി പാക്ക്</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished">തിരുത്തുക</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished">അടയ്‌ക്കുക</translation>
     </message>
@@ -645,17 +744,17 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>ഒരു ഫയൽ തിരഞ്ഞെടുക്കുക</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation>എല്ലാ ഫയലുകളും (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,9 +762,9 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 - ലേക്ക് ഉപയോക്താക്കളെ ക്ഷണിക്കുക</translation>
     </message>
     <message>
         <location line="+24"/>
@@ -676,17 +775,17 @@
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@joe:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>ചേർക്കുക</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>ക്ഷണിക്കുക</translation>
     </message>
     <message>
         <location line="+7"/>
@@ -694,6 +793,32 @@
         <translation type="unfinished">റദ്ദാക്കു</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -704,7 +829,7 @@
     <message>
         <location line="+2"/>
         <source>e.g @joe:matrix.org</source>
-        <translation type="unfinished"></translation>
+        <translation>ഉദാ. @joe:matrix.org</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -753,28 +878,28 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+19"/>
         <source>LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation>പ്രവേശിക്കുക</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -784,42 +909,60 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation>എസ് എസ് ഓ ലോഗിൻ</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed room name</source>
-        <translation type="unfinished"></translation>
+        <translation>മുറിയുടെ പേര് നീക്കം ചെയ്തു</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -829,7 +972,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+0"/>
         <source>removed topic</source>
-        <translation type="unfinished"></translation>
+        <translation>വിഷയം നീക്കം ചെയ്തു</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -862,18 +1005,23 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>ഇവരെ അനുവദിക്കുക</translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>നീക്കംചെയ്‌തു</translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -893,7 +1041,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+25"/>
         <source>Send a file</source>
-        <translation type="unfinished"></translation>
+        <translation>ഒരു ഫയൽ അയയ്ക്കുക</translation>
     </message>
     <message>
         <location line="+50"/>
@@ -901,9 +1049,9 @@ Example: https://server.my:8787</source>
         <translation>ഒരു സന്ദേശം എഴുതുക….</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>സ്റ്റിക്കറുകൾ</translation>
     </message>
     <message>
         <location line="+24"/>
@@ -924,9 +1072,9 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>തിരുത്തുക</translation>
     </message>
     <message>
         <location line="+16"/>
@@ -936,7 +1084,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+16"/>
         <source>Reply</source>
-        <translation type="unfinished"></translation>
+        <translation>മറുപടി നൽകുക</translation>
     </message>
     <message>
         <location line="+11"/>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -981,7 +1131,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+9"/>
         <source>&amp;Mark as read</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;വായിച്ചതായി കാണിക്കുക</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1059,7 +1219,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+0"/>
         <source>Deny</source>
-        <translation type="unfinished"></translation>
+        <translation>നിരസിക്കുക</translation>
     </message>
     <message>
         <location line="+13"/>
@@ -1069,6 +1229,14 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+0"/>
         <source>Accept</source>
+        <translation>സ്വീകരിക്കുക</translation>
+    </message>
+</context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1076,29 +1244,17 @@ Example: https://server.my:8787</source>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation>മൈക്രോഫോണൊന്നും കണ്ടെത്തിയില്ല.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation type="unfinished">പാസ്‍വേഡ്</translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,27 +1391,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
+        <location line="+169"/>
+        <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
+        <location line="+5"/>
+        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
-        <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished">അടയ്‌ക്കുക</translation>
     </message>
@@ -1282,10 +1444,28 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1293,7 +1473,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1302,16 +1482,6 @@ Example: https://server.my:8787</source>
         <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">അടയ്‌ക്കുക</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1414,16 +1607,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1453,7 +1666,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1468,7 +1686,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1514,19 +1742,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">എല്ലാ ഫയലുകളും (*)</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -1539,8 +1767,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1548,21 +1776,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1617,6 +1873,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">റദ്ദാക്കു</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1669,18 +2040,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1700,7 +2071,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation type="unfinished">
@@ -1709,7 +2080,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1719,7 +2090,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1739,12 +2120,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1754,12 +2135,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1769,12 +2150,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1804,32 +2190,32 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished">നിങ്ങൾ ഈ മുറിയിൽ ചേർന്നു.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1848,7 +2234,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1856,12 +2242,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1886,28 +2277,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1945,10 +2348,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1958,40 +2386,105 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+29"/>
+        <source>Room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">എല്ലാ ഫയലുകളും (*)</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -2007,8 +2500,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2016,7 +2509,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2026,22 +2519,22 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2066,7 +2559,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2081,6 +2574,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2109,7 +2612,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2175,7 +2678,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2414,17 +2957,17 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">എല്ലാ ഫയലുകളും (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2507,7 +3058,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+5"/>
         <source>LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">പ്രവേശിക്കുക</translation>
     </message>
 </context>
 <context>
@@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished">റദ്ദാക്കു</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished">റദ്ദാക്കു</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2666,32 +3186,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished">അടയ്‌ക്കുക</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3199,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2760,9 +3254,9 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">%1: %2</translation>
     </message>
     <message>
         <location line="+7"/>
@@ -2780,27 +3274,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>നിങ്ങൾ ഒരു കോൾ അവസാനിപ്പിച്ചു</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 ഒരു കോൾ അവസാനിപ്പിച്ചു</translation>
     </message>
@@ -2808,7 +3302,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <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 b21db075e5bdf27d87da6b793d5605b08f722f3d..1ff88a61715f5e9e108f71eaa5e489c564c4a5e8 100644
--- a/resources/langs/nheko_nl.ts
+++ b/resources/langs/nheko_nl.ts
@@ -4,35 +4,35 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
-        <translation type="unfinished"></translation>
+        <translation>Bellen...</translation>
     </message>
     <message>
         <location line="+10"/>
         <location line="+10"/>
         <source>Connecting...</source>
-        <translation type="unfinished"></translation>
+        <translation>Verbinden...</translation>
     </message>
     <message>
         <location line="+67"/>
         <source>You are screen sharing</source>
-        <translation type="unfinished"></translation>
+        <translation>Scherm wordt gedeeld</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Hide/Show Picture-in-Picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Toon/verberg miniatuur</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Unmute Mic</source>
-        <translation type="unfinished"></translation>
+        <translation>Microfoon aanzetten</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Mute Mic</source>
-        <translation type="unfinished"></translation>
+        <translation>Microfoon dempen</translation>
     </message>
 </context>
 <context>
@@ -40,262 +40,264 @@
     <message>
         <location filename="../qml/device-verification/AwaitingVerificationConfirmation.qml" line="+12"/>
         <source>Awaiting Confirmation</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachten op bevestiging</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Waiting for other side to complete verification.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachten op de andere kant om verificatie te voltooien.</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
 </context>
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Video oproep</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Voice Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Audio oproep</translation>
     </message>
     <message>
         <location line="+62"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Geen microfoon gevonden.</translation>
     </message>
 </context>
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Video oproep</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Voice Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Audio oproep</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Devices</source>
-        <translation type="unfinished"></translation>
+        <translation>Apparaten</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Accept</source>
-        <translation type="unfinished">Accepteren</translation>
+        <translation>Aanvaarden</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Unknown microphone: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Onbekende microfoon: %1</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Unknown camera: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Onbekende camera: %1</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Decline</source>
-        <translation type="unfinished">Afwijzen</translation>
+        <translation>Afwijzen</translation>
     </message>
     <message>
         <location line="-28"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Geen microfoon gevonden.</translation>
     </message>
 </context>
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Gehele scherm</translation>
     </message>
 </context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Gebruiker uitnodigen mislukt: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Gebruiker uitgenodigd: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
-        <translation type="unfinished"></translation>
+        <translation>Het migreren can de cache naar de huidige versie is mislukt. Dit kan verscheidene redenen hebben. Maak a.u.b een issue aan en probeer in de tussentijd een oudere versie. Je kan ook proberen de cache handmatig te verwijderen.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
-        <translation type="unfinished"></translation>
+        <translation>Bevestig deelname</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to join %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Weet je zeker dat je %1 wil binnen gaan?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
-        <translation>Kamer %1 gecreëerd.</translation>
+        <translation>Kamer %1 gemaakt.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Bevestig uitnodiging</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Weet je zeker dat je %1 (%2) wil uitnodigen?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Uitnodigen van %1 naar %2 mislukt: %3</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Confirm kick</source>
-        <translation type="unfinished"></translation>
+        <translation>Bevestig verwijdering</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to kick %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Weet je zeker dat je %1 (%2) uit de kamer wil verwijderen?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Uit kamer verwijderde gebruiker: %1</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Confirm ban</source>
-        <translation type="unfinished"></translation>
+        <translation>Bevestig verbannen</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to ban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Weet je zeker dat je gebruiker %1 (%2) wil verbannen?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Verbannen van %1 uit %2 mislukt: %3</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Banned user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Verbannen gebruiker: %1</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Confirm unban</source>
-        <translation type="unfinished"></translation>
+        <translation>Bevestig ongedaan maken verbanning</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to unban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Weet je zeker dat je %1 (%2) opnieuw wil toelaten?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Opnieuw toelaten van %1 in %2 mislukt: %3</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Unbanned user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Toegelaten gebruiker: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Weet je zeker dat je een privé chat wil beginnen met %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
-        <translation type="unfinished"></translation>
+        <translation>Migreren van de cache is mislukt!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Incompatible cache version</source>
-        <translation type="unfinished"></translation>
+        <translation>Incompatibele cacheversie</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache.</source>
-        <translation type="unfinished"></translation>
+        <translation>De opgeslagen cache is nieuwer dan deze versie van Nheko ondersteunt. Update Nheko, of verwijder je cache.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Herstellen van OLM account mislukt. Log a.u.b. opnieuw in.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Opgeslagen gegevens herstellen mislukt. Log a.u.b. opnieuw in.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
-        <translation type="unfinished"></translation>
+        <translation>Instellen van de versleuteling is mislukt. Bericht van server: %1 %2. Probeer het a.u.b. later nog eens.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Probeer a.u.b. opnieuw in te loggen: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamer binnengaan mislukt: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
-        <translation type="unfinished"></translation>
+        <translation>Je bent de kamer binnengegaan.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Failed to remove invite: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Uitnodiging verwijderen mislukt: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamer aanmaken mislukt: %1</translation>
     </message>
     <message>
         <location line="+18"/>
         <source>Failed to leave room: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamer verlaten mislukt: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Kon %1 niet verwijderen uit %2: %3</translation>
     </message>
 </context>
 <context>
@@ -303,7 +305,7 @@
     <message>
         <location filename="../qml/CommunitiesList.qml" line="+44"/>
         <source>Hide rooms with this tag or from this space by default.</source>
-        <translation type="unfinished"></translation>
+        <translation>Verberg standaard kamers met deze markering of uit deze groep.</translation>
     </message>
 </context>
 <context>
@@ -311,70 +313,70 @@
     <message>
         <location filename="../../src/timeline/CommunitiesModel.cpp" line="+37"/>
         <source>All rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Alle kamers</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Shows all rooms without filtering.</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat alles kamers zien zonder filters.</translation>
     </message>
     <message>
         <location line="+30"/>
         <source>Favourites</source>
-        <translation type="unfinished"></translation>
+        <translation>Favorieten</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms you have favourited.</source>
-        <translation type="unfinished"></translation>
+        <translation>Je favoriete kamers.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Low Priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Lage prioriteit</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms with low priority.</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamers met lage prioriteit.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Server Notices</source>
-        <translation type="unfinished"></translation>
+        <translation>Serverberichten</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Messages from your server or administrator.</source>
-        <translation type="unfinished"></translation>
+        <translation>Berichten van je server of beheerder.</translation>
     </message>
 </context>
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
-        <translation type="unfinished"></translation>
+        <translation>Ontsleutel geheimen</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Enter your recovery key or passphrase to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Voer je herstelsleutel of wachtwoordzin in om je geheimen te ontsleutelen:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Voer je herstelsleutel of wachtwoordzin in met de naam %1 om je geheimen te ontsleutelen:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Ontsleutelen mislukt</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Failed to decrypt secrets with the provided recovery key or passphrase</source>
-        <translation type="unfinished"></translation>
+        <translation>Geheimen konden niet worden ontsleuteld met de gegeven herstelsleutel of wachtwoordzin</translation>
     </message>
 </context>
 <context>
@@ -382,22 +384,22 @@
     <message>
         <location filename="../qml/device-verification/DigitVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>Verificatiecode</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Please verify the following digits. You should see the same numbers on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
-        <translation type="unfinished"></translation>
+        <translation>Controleer de volgende getallen.  Je zou dezelfde getallen moeten zien aan beide kanten.  Druk als ze verschillen op &apos;Ze komen niet overeen!&apos; om de verificatie te annuleren!</translation>
     </message>
     <message>
         <location line="+31"/>
         <source>They do not match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ze komen niet overeen!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>They match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ze zijn gelijk!</translation>
     </message>
 </context>
 <context>
@@ -405,12 +407,12 @@
     <message>
         <location filename="../../src/ui/RoomSettings.cpp" line="+42"/>
         <source>Apply</source>
-        <translation type="unfinished"></translation>
+        <translation>Toepassen</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -428,47 +430,47 @@
     <message>
         <location filename="../qml/emoji/EmojiPicker.qml" line="+68"/>
         <source>Search</source>
-        <translation type="unfinished"></translation>
+        <translation>Zoeken</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
-        <translation type="unfinished"></translation>
+        <translation>Mensen</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Nature</source>
-        <translation type="unfinished"></translation>
+        <translation>Natuur</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Food</source>
-        <translation type="unfinished"></translation>
+        <translation>Eten</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Activity</source>
-        <translation type="unfinished">Activiteit</translation>
+        <translation>Activiteiten</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Travel</source>
-        <translation type="unfinished"></translation>
+        <translation>Reizen</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Objects</source>
-        <translation type="unfinished">Objecten</translation>
+        <translation>Objecten</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Symbols</source>
-        <translation type="unfinished">Symbolen</translation>
+        <translation>Symbolen</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Flags</source>
-        <translation type="unfinished">Vlaggen</translation>
+        <translation>Vlaggen</translation>
     </message>
 </context>
 <context>
@@ -476,90 +478,88 @@
     <message>
         <location filename="../qml/device-verification/EmojiVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>Verificatiecode</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
-        <translation type="unfinished"></translation>
+        <translation>Vergelijk de volgende emoji. Je zou dezelfde moeten zien aan beide kanten. Als ze verschillen, druk dan op &apos;Ze komen niet overeen!&apos; om de verificatie te annuleren!</translation>
     </message>
     <message>
         <location line="+376"/>
         <source>They do not match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ze komen niet overeen!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>They match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Ze zijn gelijk!</translation>
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation type="unfinished"></translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Er is geen sleutel om dit bericht te ontsleutelen. We hebben de sleutel aangevraagd, maar je kan het opnieuw proberen als je ongeduldig bent.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Het bericht kon niet worden ontsleuteld, omdat we alleen een sleutel hebben voor nieuwere berichten. Je kan proberen toegang tot dit bericht aan te vragen.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation type="unfinished"></translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Er was een interne fout bij het lezen van de sleutel uit de database.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation type="unfinished"></translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Er was een fout bij het ontsleutelen van dit bericht.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Het bericht kon niet worden verwerkt.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>De versleuteling was herbruikt! Wellicht probeert iemand vervalsde berichten in dit gesprek te injecteren!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Onbekende ontsleutelingsfout</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation type="unfinished"></translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Vraag sleutel aan</translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Dit bericht is niet versleuteld!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Versleuteld door een geverifieerd apparaat</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Versleuteld door een ongeverifieerd apparaat, maar je hebt de gebruiker tot nu toe vertrouwd.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Versleuteld door een ongeverifieerd apparaat of de sleutel komt van een niet te vertrouwen bron zoals een reservesleutel.</translation>
     </message>
 </context>
 <context>
@@ -567,41 +567,125 @@
     <message>
         <location filename="../qml/device-verification/Failed.qml" line="+11"/>
         <source>Verification failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Verificatie mislukt</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Other client does not support our verification protocol.</source>
-        <translation type="unfinished"></translation>
+        <translation>De andere kant ondersteunt ons verificatieprotocol niet.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Key mismatch detected!</source>
-        <translation type="unfinished"></translation>
+        <translation>Verschil in sleutels gedetecteerd!</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
-        <translation type="unfinished"></translation>
+        <translation>Apparaatverificatie is verlopen.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
+        <translation>De andere kant heeft de verificatie geannuleerd.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
-        <source>Close</source>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+14"/>
+        <source>Close</source>
+        <translation>Sluiten</translation>
+    </message>
 </context>
 <context>
     <name>ForwardCompleter</name>
     <message>
         <location filename="../qml/ForwardCompleter.qml" line="+44"/>
         <source>Forward Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Bericht doorsturen</translation>
+    </message>
+</context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Afbeeldingspakket aanpassen</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Afbeeldingen toevoegen</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>Staatsleutel</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Afbeeldingspakketnaam</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Bronvermelding</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Gebruik als emoji</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Gebruik als sticker</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Shortcode</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Tekstinhoud</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Verwijder uit afbeeldingspakket</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Verwijder</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Annuleren</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Opslaan</translation>
     </message>
 </context>
 <context>
@@ -609,89 +693,130 @@
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Afbeeldingspakketinstellingen</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Maak pakket voor je eigen account aan</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Nieuw afbeeldingspakket voor kamer</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Privé afbeeldingspakket</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Afbeeldingspakket uit deze kamer</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Globaal geactiveerd afbeeldingspakket</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Globaal activeren</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Activeert dit afbeeldingspakket voor gebruik in alle kamers</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Bewerken</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished"></translation>
+        <translation>Sluiten</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
-        <translation type="unfinished">Kies een bestand</translation>
+        <translation>Selecteer een bestand</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Alle bestanden (*)</translation>
+        <translation>Alle bestanden (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Het is niet is gelukt om de media te versturen. Probeer het a.u.b. opnieuw.</translation>
     </message>
 </context>
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Nodig gebruikers uit naar %1</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>User ID to invite</source>
-        <translation type="unfinished">Uit te nodigen gebruikers-id</translation>
+        <translation>Gebruikers ID om uit te nodigen</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@jan:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>Toevoegen</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Uitnodigen</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
+    </message>
+</context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Kamer ID of alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Kamer verlaten</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Weet je zeker dat je de kamer wil verlaten?</translation>
     </message>
 </context>
 <context>
@@ -699,12 +824,12 @@
     <message>
         <location filename="../../src/LoginPage.cpp" line="+81"/>
         <source>Matrix ID</source>
-        <translation>Matrix-id</translation>
+        <translation>Matrix ID</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>e.g @joe:matrix.org</source>
-        <translation>b.v @jan:matrix.org&lt;</translation>
+        <translation>bijv. @jan:matrix.org</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -712,7 +837,10 @@
 You can also put your homeserver address there, if your server doesn&apos;t support .well-known lookup.
 Example: @user:server.my
 If Nheko fails to discover your homeserver, it will show you a field to enter the server manually.</source>
-        <translation type="unfinished"></translation>
+        <translation>Je inlognaam. Een mxid begint met @ gevolgd door de gebruikersnaam. Daarachter komt een dubbele punt (:) en de servernaam.
+Je kan ook het adres van je thuisserver daar invoeren, als die geen .well-known ondersteund.
+Voorbeeld: @gebruiker:mijnserver.nl
+Als Nheko je thuisserver niet kan vinden, zal er een veld verschijnen om de server handmatig in te voeren.</translation>
     </message>
     <message>
         <location line="+25"/>
@@ -722,33 +850,34 @@ If Nheko fails to discover your homeserver, it will show you a field to enter th
     <message>
         <location line="+2"/>
         <source>Your password.</source>
-        <translation type="unfinished"></translation>
+        <translation>Je wachtwoord.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Device name</source>
-        <translation type="unfinished"></translation>
+        <translation>Apparaatnaam</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.</source>
-        <translation type="unfinished"></translation>
+        <translation>Een naam voor dit apparaat, welke zichtbaar zal zijn voor anderen als ze je apparaten verifiëren. Als niets is ingevuld zal er een standaardnaam worden gebruikt.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Homeserver address</source>
-        <translation type="unfinished"></translation>
+        <translation>Thuisserveradres</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>mijnserver.nl:8787</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The address that can be used to contact you homeservers client API.
 Example: https://server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>Het adres dat gebruikt kan worden om contact te zoeken met je thuisserver&apos;s gebruikers API.
+Voorbeeld: https://mijnserver.nl:8787</translation>
     </message>
     <message>
         <location line="+19"/>
@@ -756,126 +885,149 @@ Example: https://server.my:8787</source>
         <translation>INLOGGEN</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
-        <translation type="unfinished"></translation>
+        <translation>Je hebt een ongeldige Matrix ID ingevuld. Correct voorbeeld: @jan:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatische herkenning mislukt. Ongeldig antwoord ontvangen.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatische herkenning mislukt. Onbekende fout tijdens het opvragen van .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished"></translation>
+        <translation>De vereiste aanspreekpunten werden niet gevonden. Mogelijk geen Matrix server.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ongeldig antwoord ontvangen. Zorg dat de thuisserver geldig is.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Een onbekende fout trad op. Zorg dat de thuisserver geldig is.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation>SSO INLOGGEN</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Leeg wachtwoord</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
+        <translation>SSO inloggen mislukt</translation>
+    </message>
+</context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
-        <translation type="unfinished"></translation>
+        <translation>verwijderd</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Encryption enabled</source>
-        <translation type="unfinished"></translation>
+        <translation>Versleuteling geactiveerd</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>kamernaam veranderd in: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed room name</source>
-        <translation type="unfinished"></translation>
+        <translation>kamernaam verwijderd</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>topic changed to: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>onderwerp aangepast naar: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed topic</source>
-        <translation type="unfinished"></translation>
+        <translation>onderwerp verwijderd</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 changed the room avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft de kameravatar veranderd</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 created and configured room: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte en configureerde de kamer: %2</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>%1 placed a voice call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 plaatste een spraakoproep.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 placed a video call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 plaatste een video oproep.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 placed a call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 plaatste een oproep.</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>%1 answered the call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 beantwoordde de oproep.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 ended the call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 beëindigde de oproep.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Negotiating call...</source>
-        <translation type="unfinished"></translation>
+        <translation>Onderhandelen oproep…</translation>
+    </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Binnenlaten</translation>
     </message>
 </context>
 <context>
@@ -883,134 +1035,141 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/MessageInput.qml" line="+44"/>
         <source>Hang up</source>
-        <translation type="unfinished"></translation>
+        <translation>Ophangen</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Place a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Plaats een oproep</translation>
     </message>
     <message>
         <location line="+25"/>
         <source>Send a file</source>
-        <translation type="unfinished"></translation>
+        <translation>Verstuur een bestand</translation>
     </message>
     <message>
         <location line="+50"/>
         <source>Write a message...</source>
-        <translation type="unfinished">Typ een bericht...</translation>
+        <translation>Typ een bericht…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>Stickers</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>Emoji</source>
-        <translation type="unfinished"></translation>
+        <translation>Emoji</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Send</source>
-        <translation type="unfinished"></translation>
+        <translation>Verstuur</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>You don&apos;t have permission to send messages in this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Je hebt geen toestemming om berichten te versturen in deze kamer</translation>
     </message>
 </context>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>Bewerken</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>React</source>
-        <translation type="unfinished"></translation>
+        <translation>Reageren</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Reply</source>
-        <translation type="unfinished"></translation>
+        <translation>Beantwoorden</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Options</source>
-        <translation type="unfinished"></translation>
+        <translation>Opties</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Kopiëren</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
-        <translation type="unfinished"></translation>
+        <translation>Kopieer &amp;link</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
-        <translation type="unfinished"></translation>
+        <translation>Re&amp;ageren</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Repl&amp;y</source>
-        <translation type="unfinished"></translation>
+        <translation>Beantwoo&amp;rden</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>B&amp;ewerken</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Read receip&amp;ts</source>
-        <translation type="unfinished"></translation>
+        <translation>Leesbeves&amp;tigingen</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>&amp;Forward</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Doorsturen</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>&amp;Mark as read</source>
-        <translation type="unfinished"></translation>
+        <translation>Gelezen &amp;markeren</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>View raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Ruw bericht bekijken</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>View decrypted raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Ontsleuteld ruw bericht bekijken</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Remo&amp;ve message</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Verwijder bericht</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Save as</source>
-        <translation type="unfinished"></translation>
+        <translation>Op&amp;slaan als</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Open in external program</source>
-        <translation type="unfinished"></translation>
+        <translation>In extern programma &amp;openen</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Copy link to eve&amp;nt</source>
+        <translation>Kopieer link naar gebeurte&amp;nis</translation>
+    </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1019,101 +1178,102 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/NewVerificationRequest.qml" line="+11"/>
         <source>Send Verification Request</source>
-        <translation type="unfinished"></translation>
+        <translation>Verstuur verificatieverzoek</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Received Verification Request</source>
+        <translation>Ontvangen verificatieverzoek</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
-        <translation type="unfinished"></translation>
+        <translation>Om andere gebruikers te laten weten welke apparaten echt van jou zijn, kan je ze verifiëren. Dit zorgt ook dat reservesleutels automatisch werken. Nu %1 verifiëren?</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.</source>
-        <translation type="unfinished"></translation>
+        <translation>Om zeker te zijn dat niemand meeleest met je versleutelde gesprek kan je de andere kant verifiëren.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 has requested to verify their device %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft verzocht om hun apparaat %2 te verifiëren.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 using the device %2 has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1, gebruikmakend van apparaat %2 heeft verzocht om verificatie.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your device (%1) has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>Je apparaat (%1) heeft verzocht om verificatie.</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Deny</source>
-        <translation type="unfinished"></translation>
+        <translation>Weigeren</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Start verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Begin verificatie</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Accept</source>
-        <translation type="unfinished">Accepteren</translation>
+        <translation>Accepteren</translation>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
+        <translation>%1 stuurde een versleuteld bericht</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 antwoordde: %2</translation>
     </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
         <source>%1 replied with an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 antwoordde met een versleuteld bericht</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>%1 replied to a message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 antwoordde op een bericht</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>%1 sent a message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 stuurde een bericht</translation>
     </message>
 </context>
 <context>
@@ -1121,32 +1281,32 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/PlaceCall.qml" line="+48"/>
         <source>Place a call to %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Bel %1?</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Geen microfoon gevonden.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
-        <translation type="unfinished"></translation>
+        <translation>Spraak</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Video</source>
-        <translation type="unfinished"></translation>
+        <translation>Video</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Scherm</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
 </context>
 <context>
@@ -1154,49 +1314,65 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/delegates/Placeholder.qml" line="+11"/>
         <source>unimplemented event: </source>
-        <translation type="unfinished"></translation>
+        <translation>Niet geïmplementeerd evenement: </translation>
     </message>
 </context>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
-        <translation type="unfinished"></translation>
+        <translation>Creëer een uniek profiel, waardoor je op meerdere accounts tegelijk kan inloggen, en meerdere kopieën van Nheko tegelijk kan starten.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>profile</source>
-        <translation type="unfinished"></translation>
+        <translation>profiel</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>profile name</source>
-        <translation type="unfinished"></translation>
+        <translation>profielnaam</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Leesbevestigingen</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Gisteren, %1</translation>
     </message>
 </context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Gebruikersnaam</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
-        <translation type="unfinished"></translation>
+        <translation>De gebruikersnaam mag niet leeg zijn, en mag alleen de volgende tekens bevatten: a-z, 0-9, ., _, =, -, en /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Wachtwoord</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Please choose a secure password. The exact requirements for password strength may depend on your server.</source>
-        <translation type="unfinished"></translation>
+        <translation>Kies a.u.b. een veilig wachtwoord. De exacte vereisten voor een wachtwoord kunnen per server verschillen.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -1206,12 +1382,12 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+4"/>
         <source>Homeserver</source>
-        <translation type="unfinished"></translation>
+        <translation>Thuisserver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
-        <translation type="unfinished"></translation>
+        <translation>Een server die registratie toestaat. Omdat Matrix gedecentraliseerd is, moet je eerst zelf een server vinden om je op te registeren, of je eigen server hosten.</translation>
     </message>
     <message>
         <location line="+35"/>
@@ -1219,52 +1395,42 @@ Example: https://server.my:8787</source>
         <translation>REGISTREREN</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatische herkenning mislukt. Onjuist gevormd antwoord ontvangen.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatische herkenning mislukt. Onbekende fout bij opvragen van .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished"></translation>
+        <translation>De vereiste aanspreekpunten konden niet worden gevonden. Mogelijk geen Matrix server.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Onjuist gevormd antwoord ontvangen. Zorg dat de thuisserver geldig is.</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Een onbekende fout trad op. Zorg dat de thuisserver geldig is.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Het wachtwoord is niet lang genoeg (minimaal 8 tekens)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>De wachtwoorden komen niet overeen</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Ongeldige servernaam</translation>
     </message>
@@ -1272,294 +1438,388 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
-        <translation type="unfinished"></translation>
+        <translation>Sluiten</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Cancel edit</source>
+        <translation>Bewerken annuleren</translation>
+    </message>
+</context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Verken openbare kamers</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Zoek naar openbare kamers</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
-        <translation type="unfinished"></translation>
+        <translation>geen versie opgeslagen</translation>
     </message>
 </context>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
-        <translation type="unfinished"></translation>
+        <translation>Nieuwe markering</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter the tag you want to use:</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
+        <translation>Voer de markering in die je wil gebruiken:</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
-        <translation type="unfinished">Kamer verlaten</translation>
+        <translation>Kamer verlaten</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Tag room as:</source>
-        <translation type="unfinished"></translation>
+        <translation>Markeer kamer als:</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Favourite</source>
-        <translation type="unfinished"></translation>
+        <translation>Favoriet</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Low priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Lage prioriteit</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Server notice</source>
-        <translation type="unfinished"></translation>
+        <translation>Serverbericht</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Create new tag...</source>
-        <translation type="unfinished"></translation>
+        <translation>Maak nieuwe markering...</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Statusbericht</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter your status message:</source>
-        <translation type="unfinished"></translation>
+        <translation>Voer je statusbericht in:</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Profile settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Profielinstellingen</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Set status message</source>
-        <translation type="unfinished"></translation>
+        <translation>Stel statusbericht in</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
+        <translation>Uitloggen</translation>
+    </message>
+    <message>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
-        <source>Start a new chat</source>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Sluiten</translation>
+    </message>
+    <message>
+        <location line="+65"/>
+        <source>Start a new chat</source>
+        <translation>Nieuwe chat beginnen</translation>
+    </message>
     <message>
         <location line="+8"/>
         <source>Join a room</source>
-        <translation type="unfinished">Kamer betreden</translation>
+        <translation>Kamer binnengaan</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Create a new room</source>
-        <translation type="unfinished"></translation>
+        <translation>Nieuwe kamer aanmaken</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Room directory</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamerlijst</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Gebruikersinstellingen</translation>
     </message>
 </context>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Deelnemers in %1</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%n persoon in %1</numerusform>
+            <numerusform>%n personen in %1</numerusform>
         </translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Invite more people</source>
-        <translation type="unfinished"></translation>
+        <translation>Nodig meer mensen uit</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Deze kamer is niet versleuteld!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Deze gebruiker is geverifieerd.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Deze gebruiker is niet geverifieerd, maar gebruikt nog dezelfde hoofdsleutel als de eerste keer.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Deze gebruiker heeft ongeverifieerde apparaten!</translation>
     </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamerinstellingen</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 deelnemer(s)</translation>
     </message>
     <message>
         <location line="+55"/>
         <source>SETTINGS</source>
-        <translation type="unfinished"></translation>
+        <translation>INSTELLINGEN</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Notifications</source>
-        <translation type="unfinished"></translation>
+        <translation>Meldingen</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Muted</source>
-        <translation type="unfinished"></translation>
+        <translation>Gedempt</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Mentions only</source>
-        <translation type="unfinished"></translation>
+        <translation>Alleen vermeldingen</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All messages</source>
-        <translation type="unfinished"></translation>
+        <translation>Alle berichten</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Kamertoegang</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
-        <translation type="unfinished"></translation>
+        <translation>Iedereen (inclusief gasten)</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Anyone</source>
-        <translation type="unfinished"></translation>
+        <translation>Iedereen</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Invited users</source>
-        <translation type="unfinished"></translation>
+        <translation>Uitgenodigde gebruikers</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>Door aan te kloppen</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Beperkt door deelname aan andere kamers</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
-        <translation type="unfinished"></translation>
+        <translation>Versleuteling</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>End-to-End Encryption</source>
-        <translation type="unfinished"></translation>
+        <translation>Eind-tot-eind versleuteling</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
                             Please take note that it can&apos;t be disabled afterwards.</source>
-        <translation type="unfinished"></translation>
+        <translation>Versleuteling is momenteel experimenteel en dingen kunnen onverwacht stuk gaan.&lt;br&gt;Let op: versleuteling kan achteraf niet uitgeschakeld worden.</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Sticker &amp; Emoji instellingen</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Bewerken</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Verander welke afbeeldingspakketten zijn ingeschakeld, verwijder ze of voeg nieuwe toe</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>INFO</source>
-        <translation type="unfinished"></translation>
+        <translation>INFO</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Internal ID</source>
-        <translation type="unfinished"></translation>
+        <translation>Interne ID</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Room Version</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamerversie</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Versleuteling kon niet worden ingeschakeld: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>Kies een avatar</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Alle bestanden (*)</translation>
+        <translation>Alle bestanden (*)</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>The selected file is not an image</source>
-        <translation type="unfinished"></translation>
+        <translation>Het gekozen bestand is geen afbeelding</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Error while reading file: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Fout bij lezen van bestand: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
-        <translation type="unfinished"></translation>
+        <translation>Uploaden van afbeelding mislukt: %1</translation>
     </message>
 </context>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachtende uitnodiging.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Voorbeeld van deze kamer</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
+        <translation>Geen voorbeeld beschikbaar</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1568,53 +1828,168 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/ScreenShare.qml" line="+30"/>
         <source>Share desktop with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Scherm delen met %1?</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Window:</source>
-        <translation type="unfinished"></translation>
+        <translation>Scherm:</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Frame rate:</source>
-        <translation type="unfinished"></translation>
+        <translation>Verversingssnelheid:</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Include your camera picture-in-picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat eigen cameraminiatuur zien</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Request remote camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Verzoek om camera van de andere kant</translation>
     </message>
     <message>
         <location line="+1"/>
         <location line="+9"/>
         <source>View your callee&apos;s camera like a regular video call</source>
-        <translation type="unfinished"></translation>
+        <translation>Bekijk de camera van degene die belt zoals bij een regulier videogesprek</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Hide mouse cursor</source>
-        <translation type="unfinished"></translation>
+        <translation>Verstop muiscursor</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Share</source>
-        <translation type="unfinished"></translation>
+        <translation>Delen</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Preview</source>
-        <translation type="unfinished"></translation>
+        <translation>Voorbeeld</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
+    </message>
+</context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Verbinden met geheimopslag mislukt</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Kon afbeeldingspakket niet updaten: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Kon oud afbeeldingspakket niet verwijderen: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Kon afbeelding niet openen: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Kon afbeelding niet uploaden: %1</translation>
     </message>
 </context>
 <context>
@@ -1622,22 +1997,22 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/StatusIndicator.qml" line="+24"/>
         <source>Failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Mislukt</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Sent</source>
-        <translation type="unfinished"></translation>
+        <translation>Verstuurd</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Received</source>
-        <translation type="unfinished"></translation>
+        <translation>Ontvangen</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Read</source>
-        <translation type="unfinished"></translation>
+        <translation>Gelezen</translation>
     </message>
 </context>
 <context>
@@ -1645,7 +2020,7 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
         <source>Search</source>
-        <translation type="unfinished"></translation>
+        <translation>Zoeken</translation>
     </message>
 </context>
 <context>
@@ -1653,283 +2028,315 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/Success.qml" line="+11"/>
         <source>Successful Verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Succesvolle verificatie</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Verification successful! Both sides verified their devices!</source>
-        <translation type="unfinished"></translation>
+        <translation>Verificatie gelukt!  Beide kanten hebben hun apparaat geverifieerd!</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Close</source>
-        <translation type="unfinished"></translation>
+        <translation>Sluiten</translation>
     </message>
 </context>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Bericht intrekken mislukt: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
-        <translation type="unfinished"></translation>
+        <translation>Kon evenement niet versleutelen, versturen geannuleerd!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
-        <translation type="unfinished">Afbeelding opslaan</translation>
+        <translation>Afbeelding opslaan</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save video</source>
-        <translation type="unfinished"></translation>
+        <translation>Video opslaan</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save audio</source>
-        <translation type="unfinished"></translation>
+        <translation>Audio opslaan</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save file</source>
-        <translation type="unfinished"></translation>
+        <translation>Bestand opslaan</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%1%2 is aan het typen.</numerusform>
+            <numerusform>%1 en %2 zijn aan het typen.</numerusform>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte de kamer openbaar.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 made this room require and invitation to join.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte deze kamer uitnodiging-vereist.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 maakte deze kamer aanklopbaar.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 stond toe dat deelnemers aan de volgende kamers automatisch mogen deelnemen: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte de kamer openbaar voor gasten.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 has closed the room to guest access.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte de kamer gesloten voor gasten.</translation>
     </message>
     <message>
         <location line="+23"/>
         <source>%1 made the room history world readable. Events may be now read by non-joined people.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte de kamergeschiedenis openbaar. Niet-deelnemers kunnen nu de kamer inzien.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>%1 set the room history visible to members from this point on.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte de kamergeschiedenis deelname-vereist.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte de kamergeschiedenis zichtbaar vanaf het moment van uitnodigen.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 maakte de kamergeschiedenis zichtbaar vanaf moment van deelname.</translation>
     </message>
     <message>
         <location line="+22"/>
         <source>%1 has changed the room&apos;s permissions.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft de rechten van de kamer aangepast.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 is uitgenodigd.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 is van avatar veranderd.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 changed some profile info.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft wat profielinformatie aangepast.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 neemt nu deel.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 neemt deel via autorisatie van %2&apos;s server.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft de uitnodiging geweigerd.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Revoked the invite to %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Uitnodiging van %1 is ingetrokken.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 left the room.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft de kamer verlaten.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Kicked %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 is verwijderd.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Unbanned %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 is opnieuw toegelaten.</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>%1 was banned.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 is verbannen.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Reden: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft het aankloppen ingetrokken.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
-        <translation type="unfinished">Je bent lid geworden van deze kamer.</translation>
+        <translation>Je neemt nu deel aan deze kamer.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 is van avatar veranderd en heet nu %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heet nu %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Aankloppen van %1 geweigerd.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 left after having already left!</source>
         <comment>This is a leave event after the user already left and shouldn&apos;t happen apart from state resets</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 is vertrokken na reeds vertrokken te zijn!</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>%1 knocked.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 klopt aan.</translation>
     </message>
 </context>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
-        <translation type="unfinished"></translation>
+        <translation>Bewerkt</translation>
     </message>
 </context>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
-        <translation type="unfinished"></translation>
+        <translation>Geen kamer open</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Geen voorbeeld beschikbaar</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 deelnemer(s)</translation>
     </message>
     <message>
         <location line="+33"/>
         <source>join the conversation</source>
-        <translation type="unfinished"></translation>
+        <translation>Neem deel aan het gesprek</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>accept invite</source>
-        <translation type="unfinished"></translation>
+        <translation>accepteer uitnodiging</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>decline invite</source>
-        <translation type="unfinished"></translation>
+        <translation>wijs uitnodiging af</translation>
     </message>
     <message>
         <location line="+27"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Terug naar kamerlijst</translation>
     </message>
 </context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
+        <translation>Terug naar kamerlijst</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
+        <translation>Geen kamer geselecteerd</translation>
+    </message>
+    <message>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Deze kamer is niet versleuteld!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Deze kamer bevat alleen geverifieerde apparaten.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Deze kamer bevat ongeverifieerde apparaten!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
-        <translation type="unfinished"></translation>
+        <translation>Kameropties</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Invite users</source>
-        <translation type="unfinished">Gebruikers uitnodigen</translation>
+        <translation>Gebruikers uitnodigen</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Members</source>
-        <translation type="unfinished">Leden</translation>
+        <translation>Deelnemers</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Leave room</source>
-        <translation type="unfinished">Kamer verlaten</translation>
+        <translation>Kamer verlaten</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Settings</source>
-        <translation type="unfinished">Instellingen</translation>
+        <translation>Instellingen</translation>
     </message>
 </context>
 <context>
@@ -1945,78 +2352,168 @@ Example: https://server.my:8787</source>
         <translation>Afsluiten</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Voer a.u.b een geldig registratieteken in.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
-        <translation type="unfinished"></translation>
+        <translation>Globaal gebruikersprofiel</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Room User Profile</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamerspecifiek gebruikersprofiel</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Verander avatar globaal.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Verander avatar. Heeft alleen effect op deze kamer.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Verander weergavenaam globaal.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Verander weergavenaam. Heeft alleen effect op deze kamer.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Kamer: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>Dit is een kamer-specifiek profiel. De weergavenaam en avatar kunnen verschillen van de globale versie.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Open het globale profiel van deze gebruiker.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
+        <translation>Verifiëren</translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Begin een privéchat.</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Verwijder de gebruiker.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Verban de gebruiker.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+31"/>
+        <source>Change device name.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+27"/>
         <source>Unverify</source>
+        <translation>On-verifiëren</translation>
+    </message>
+    <message>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location line="+223"/>
         <source>Select an avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>Kies een avatar</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Alle bestanden (*)</translation>
+        <translation>Alle bestanden (*)</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>The selected file is not an image</source>
-        <translation type="unfinished"></translation>
+        <translation>Het gekozen bestand is geen afbeelding</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Error while reading file: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Fout bij lezen bestand: %1</translation>
     </message>
 </context>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Standaard</translation>
     </message>
 </context>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Minimaliseren naar systeemvak</translation>
     </message>
@@ -2026,145 +2523,163 @@ Example: https://server.my:8787</source>
         <translation>Geminimaliseerd opstarten</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
-        <translation>Zijbalk van groep</translation>
+        <translation>Zijbalk met groepen</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
-        <translation type="unfinished"></translation>
+        <translation>Ronde avatars</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>profiel: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Standaard</translation>
     </message>
     <message>
         <location line="+31"/>
         <source>CALLS</source>
-        <translation type="unfinished"></translation>
+        <translation>OPROEPEN</translation>
     </message>
     <message>
         <location line="+46"/>
         <source>Cross Signing Keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Kruisversleutelingssleutels</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>REQUEST</source>
-        <translation type="unfinished"></translation>
+        <translation>OPVRAGEN</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>DOWNLOAD</source>
-        <translation type="unfinished"></translation>
+        <translation>DOWNLOADEN</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
-        <translation type="unfinished"></translation>
+        <translation>Blijf draaien in de achtergrond na het sluiten van het scherm.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Start the application in the background without showing the client window.</source>
-        <translation type="unfinished"></translation>
+        <translation>Start de applicatie in de achtergrond zonder het scherm te tonen.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change the appearance of user avatars in chats.
 OFF - square, ON - Circle.</source>
+        <translation>Verander het uiterlijk van avatars in de chats.
+UIT - vierkant, AAN - cirkel.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat een kolom zien met groepen en markeringen naast de kamerlijst.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Decrypt messages in sidebar</source>
-        <translation type="unfinished"></translation>
+        <translation>Ontsleutel berichten in de zijbalk</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Decrypt the messages shown in the sidebar.
 Only affects messages in encrypted chats.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ontsleutel de berichten getoond in de zijbalk.
+Heeft alleen effect op versleutelde chats.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Privacy Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Privacy scherm</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>When the window loses focus, the timeline will
 be blurred.</source>
-        <translation type="unfinished"></translation>
+        <translation>Als het scherm focus verliest, zal de tijdlijn
+worden geblurt.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
-        <translation type="unfinished"></translation>
+        <translation>Privacy scherm wachttijd (in seconden [0 - 3600])</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set timeout (in seconds) for how long after window loses
 focus before the screen will be blurred.
 Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds)</source>
-        <translation type="unfinished"></translation>
+        <translation>Stel wachttijd (in seconden) voor hoe lang het duurt nadat
+focus weg is voordat het scherm wordt geblurt.
+Kies 0 om direct te blurren. Maximale waarde is 1 uur (3600 seconden)</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Show buttons in timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat knoppen zien in tijdlijn</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show buttons to quickly reply, react or access additional options next to each message.</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat knoppen zien om snel te reageren, beantwoorden, of extra opties te kunnen gebruiken naast elk bericht.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Limit width of timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Beperk breedte van tijdlijn</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised</source>
-        <translation type="unfinished"></translation>
+        <translation>Stel de maximale breedte in van berichten in de tijdlijn (in pixels). Dit kan helpen bij de leesbaarheid als Nheko gemaximaliseerd is.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Typing notifications</source>
-        <translation>Meldingen bij typen van berichten</translation>
+        <translation>Typnotificaties</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show who is typing in a room.
 This will also enable or disable sending typing notifications to others.</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat zien wie er typt in een kamer.
+Dit schakelt ook het versturen van je eigen typnotificaties naar anderen in of uit.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Sort rooms by unreads</source>
-        <translation type="unfinished"></translation>
+        <translation>Sorteer kamers op ongelezen berichten</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Display rooms with new messages first.
 If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room.
 If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don&apos;t seem to consider them as important as the other rooms.</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat kamers met nieuwe berichten eerst zien.
+Indien uitgeschakeld, staan kamers gesorteerd op de tijd van het laatst ontvangen bericht.
+Indien ingeschakeld, staan kamers met actieve notificaties (het cirkeltje met een getal erin) bovenaan. Kamers die je hebt gedempt zullen nog steeds op tijd zijn gesorteerd, want die vind je blijkbaar niet zo belangrijk als de andere kamers.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Leesbevestigingen</translation>
     </message>
@@ -2172,94 +2687,137 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <location line="+2"/>
         <source>Show if your message was read.
 Status is displayed next to timestamps.</source>
-        <translation type="unfinished"></translation>
+        <translation>Laat zien of je bericht gelezen is.
+De status staat naast de tijdsindicatie.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
-        <translation type="unfinished"></translation>
+        <translation>Verstuur berichten in Markdown</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Allow using markdown in messages.
 When disabled, all messages are sent as a plain text.</source>
-        <translation type="unfinished"></translation>
+        <translation>Sta het gebruik van Markdown in berichten toe.
+Indien uitgeschakeld worden alle berichten als platte tekst verstuurd.</translation>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Speel animaties in afbeeldingen alleen af tijdens muisover</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
-        <translation type="unfinished"></translation>
+        <translation>Bureaubladnotificaties</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Notify about received message when the client is not currently focused.</source>
-        <translation type="unfinished"></translation>
+        <translation>Verstuur een notificatie over ontvangen berichten als het scherm geen focus heeft.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Alert on notification</source>
-        <translation type="unfinished"></translation>
+        <translation>Melding bij notificatie</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show an alert when a message is received.
 This usually causes the application icon in the task bar to animate in some fashion.</source>
-        <translation type="unfinished"></translation>
+        <translation>Activeer een melding als een bericht binnen komt.
+Meestal zorgt dit dat het icoon in de taakbalk op een manier animeert of iets dergelijks.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Highlight message on hover</source>
-        <translation type="unfinished"></translation>
+        <translation>Oplichten van berichten onder muis</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the background color of messages when you hover over them.</source>
-        <translation type="unfinished"></translation>
+        <translation>Veranderd de achtergrondkleur van het bericht waar de muiscursor op staat.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Large Emoji in timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Grote emoji in de tijdlijn</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Make font size larger if messages with only a few emojis are displayed.</source>
-        <translation type="unfinished"></translation>
+        <translation>Maakt het lettertype groter als berichten met slechts enkele emoji worden getoond.</translation>
+    </message>
+    <message>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Verstuur alleen versleutelde berichten naar geverifieerde gebruikers</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Vereist dat een gebruiker geverifieerd is voordat berichten worden versleuteld. Verbetert de beveiliging maar maakt versleutelen irritanter om in te stellen.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
-        <translation type="unfinished"></translation>
+        <translation>Deel sleutels met geverifieerde gebruikers en apparaten</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Beantwoord automatisch sleutelverzoeken van andere gebruikers, indien geverifieerd, ook als dat apparaat normaal geen toegang tot die sleutels had moeten hebben.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Online reservesleutel</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Download van en upload naar de online reservesleutel.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Activeer online reservesleutelopslag</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>De Nheko auteurs raden af om online reservesleutelopslag te gebruiken totdat symmetrische reservesleutelopslag beschikbaar is. Toch activeren?</translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>IN CACHE</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>NOT CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>NIET IN CACHE</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
-        <translation type="unfinished"></translation>
+        <translation>Schaalfactor</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the scale factor of the whole user interface.</source>
-        <translation type="unfinished"></translation>
+        <translation>Verander de schaalfactor van de gehele gebruikersinterface.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Font size</source>
-        <translation type="unfinished"></translation>
+        <translation>Lettertypegrootte</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Font Family</source>
-        <translation type="unfinished"></translation>
+        <translation>Lettertype</translation>
     </message>
     <message>
         <location line="+8"/>
@@ -2269,194 +2827,202 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+4"/>
         <source>Ringtone</source>
-        <translation type="unfinished"></translation>
+        <translation>Beltoon</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set the notification sound to play when a call invite arrives</source>
-        <translation type="unfinished"></translation>
+        <translation>Stel het geluid in dat speelt als een oproep binnen komt</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Microphone</source>
-        <translation type="unfinished"></translation>
+        <translation>Microfoon</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Camera</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera resolution</source>
-        <translation type="unfinished"></translation>
+        <translation>Cameraresolutie</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera frame rate</source>
-        <translation type="unfinished"></translation>
+        <translation>Cameraverversingssnelheid</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Allow fallback call assist server</source>
-        <translation type="unfinished"></translation>
+        <translation>Sta terugval naar oproepassistentieserver toe</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will use turn.matrix.org as assist when your home server does not offer one.</source>
-        <translation type="unfinished"></translation>
+        <translation>Zal turn.matrix.org gebruiken om te assisteren als je thuisserver geen TURN server heeft.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Device ID</source>
-        <translation type="unfinished"></translation>
+        <translation>Apparaat ID</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Device Fingerprint</source>
-        <translation type="unfinished"></translation>
+        <translation>Apparaat vingerafdruk</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Sessiesleutels</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>IMPORT</source>
-        <translation type="unfinished"></translation>
+        <translation>IMPORTEREN</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>EXPORT</source>
-        <translation type="unfinished"></translation>
+        <translation>EXPORTEREN</translation>
     </message>
     <message>
         <location line="-34"/>
         <source>ENCRYPTION</source>
-        <translation type="unfinished"></translation>
+        <translation>VERSLEUTELING</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>ALGEMEEN</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
-        <translation type="unfinished"></translation>
+        <translation>INTERFACE</translation>
+    </message>
+    <message>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Speelt media zoals GIFs en WebPs alleen af terwijl de muiscursor erboven hangt.</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
-        <translation type="unfinished"></translation>
+        <translation>Touchscreenmodus</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will prevent text selection in the timeline to make touch scrolling easier.</source>
-        <translation type="unfinished"></translation>
+        <translation>Voorkomt dat tekst geselecteerd wordt in de tijdlijn, om scrollen makkelijker te maken.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Emoji Font Family</source>
-        <translation type="unfinished"></translation>
+        <translation>Emoji lettertype</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Hoofdsleutel</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your most important key. You don&apos;t need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys.</source>
-        <translation type="unfinished"></translation>
+        <translation>Je belangrijkste sleutel. Deze hoeft niet gecached te zijn, en dat maakt het minder waarschijnlijk dat hij ooit gestolen wordt. Hij is alleen nodig om je andere sleutels te roteren.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>User signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Gebruikerssleutel</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify other users. If it is cached, verifying a user will verify all their devices.</source>
-        <translation type="unfinished"></translation>
+        <translation>De sleutel die wordt gebruikt om andere gebruikers te verifiëren. Indien gecached zal het verifiëren van een gebruiker alle apparaten van die gebruiker verifiëren.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Zelf ondertekenen sleutel</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you.</source>
-        <translation type="unfinished"></translation>
+        <translation>De sleutel om je eigen apparaten mee te verifiëren. Indien gecached zal één van je apparaten verifiëren dat doen voor andere apparaten en gebruikers die jou geverifieerd hebben.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Backup key</source>
-        <translation type="unfinished"></translation>
+        <translation>Reservesleutel</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server.</source>
-        <translation type="unfinished"></translation>
+        <translation>De sleutel om online reservesleutels mee te ontsleutelen. Indien gecached kan je online reservesleutel activeren om je sleutels veilig versleuteld op de server op te slaan.</translation>
     </message>
     <message>
         <location line="+54"/>
         <source>Select a file</source>
-        <translation type="unfinished">Kies een bestand</translation>
+        <translation>Kies een bestand</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Alle bestanden (*)</translation>
+        <translation>Alle bestanden (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
-        <translation type="unfinished"></translation>
+        <translation>Open sessiebestand</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
         <source>Error</source>
-        <translation type="unfinished"></translation>
+        <translation>Fout</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachtwoord voor bestand</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
-        <translation type="unfinished"></translation>
+        <translation>Voer de wachtwoordzin in om het bestand te ontsleutelen:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
-        <translation type="unfinished"></translation>
+        <translation>Het wachtwoord kan niet leeg zijn</translation>
     </message>
     <message>
         <location line="-8"/>
         <source>Enter passphrase to encrypt your session keys:</source>
-        <translation type="unfinished"></translation>
+        <translation>Voer wachtwoordzin in om je sessiesleutels mee te versleutelen:</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>File to save the exported session keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Bestand om geëxporteerde sessiesleutels in op te slaan</translation>
+    </message>
+</context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Geen versleutelde chat gevonden met deze gebruiker. Maak een versleutelde chat aan met deze gebruiker en probeer het opnieuw.</translation>
     </message>
 </context>
 <context>
@@ -2464,27 +3030,27 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../qml/device-verification/Waiting.qml" line="+12"/>
         <source>Waiting for other party…</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachten op andere kant…</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Waiting for other side to accept the verification request.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachten op de andere kant om het verificatieverzoek te accepteren.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to continue the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachten op de andere kant om het verificatieproces voort te zetten.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to complete the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wachten op de andere kant om het verificatieproces af te ronden.</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
 </context>
 <context>
@@ -2492,7 +3058,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/WelcomePage.cpp" line="+34"/>
         <source>Welcome to nheko! The desktop client for the Matrix protocol.</source>
-        <translation>Welkom bij nheko! Dé computerclient voor het Matrix-protocol.</translation>
+        <translation>Welkom bij Nheko! De bureaubladclient voor het Matrix-protocol.</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2515,7 +3081,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/Utils.cpp" line="+184"/>
         <source>Yesterday</source>
-        <translation type="unfinished"></translation>
+        <translation>Gisteren</translation>
     </message>
 </context>
 <context>
@@ -2523,12 +3089,12 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/CreateRoom.cpp" line="+40"/>
         <source>Create room</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamer maken</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -2553,7 +3119,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+8"/>
         <source>Room Preset</source>
-        <translation>Kamer-voorinstellingen</translation>
+        <translation>Kamer voorinstellingen</translation>
     </message>
     <message>
         <location line="+9"/>
@@ -2566,53 +3132,22 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/FallbackAuth.cpp" line="+34"/>
         <source>Open Fallback in Browser</source>
-        <translation type="unfinished"></translation>
+        <translation>Open fallback in browser</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Confirm</source>
-        <translation type="unfinished"></translation>
+        <translation>Bevestigen</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Open the fallback, follow the steps and confirm after completing them.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Kamer-id of alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Weet je zeker dat je wilt vertrekken?</translation>
+        <translation>Open de fallback, volg de stappen, en bevestig nadat je klaar bent.</translation>
     </message>
 </context>
 <context>
@@ -2620,7 +3155,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/Logout.cpp" line="+35"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
     <message>
         <location line="+8"/>
@@ -2645,7 +3180,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <source>Media type: %1
 Media size: %2
 </source>
-        <translation>Mediasoort: %1
+        <translation>Mediatype: %1
 Mediagrootte: %2
 </translation>
     </message>
@@ -2655,12 +3190,12 @@ Mediagrootte: %2
     <message>
         <location filename="../../src/dialogs/ReCaptcha.cpp" line="+35"/>
         <source>Cancel</source>
-        <translation type="unfinished">Annuleren</translation>
+        <translation>Annuleren</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Confirm</source>
-        <translation type="unfinished"></translation>
+        <translation>Bevestigen</translation>
     </message>
     <message>
         <location line="+11"/>
@@ -2668,151 +3203,125 @@ Mediagrootte: %2
         <translation>Los de reCAPTCHA op en klik op &apos;Bevestigen&apos;</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Leesbevestigingen</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
         <location filename="../../src/Utils.h" line="+115"/>
         <source>You sent an audio clip</source>
-        <translation type="unfinished"></translation>
+        <translation>Je verstuurde een audio clip</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent an audio clip</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 verstuurde een audio clip</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
-        <translation type="unfinished"></translation>
+        <translation>Je verstuurde een afbeelding</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 verstuurde een afbeelding</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
-        <translation type="unfinished"></translation>
+        <translation>Je verstuurde een bestand</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 verstuurde een bestand</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
-        <translation type="unfinished"></translation>
+        <translation>Je verstuurde een video</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 verstuurde een video</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
-        <translation type="unfinished"></translation>
+        <translation>Je verstuurde een sticker</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 verstuurde een sticker</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
-        <translation type="unfinished"></translation>
+        <translation>Je verstuurde een notificatie</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent a notification</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 verstuurde een notificatie</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>You: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Jij: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1: %2</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>You sent an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>Je hebt een versleuteld bericht verstuurd</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 heeft een versleuteld bericht verstuurd</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>You placed a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Je hebt een oproep geplaatst</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 plaatste een oproep</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Je beantwoordde een oproep</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 beantwoordde een oproep</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Je hing op</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 hing op</translation>
     </message>
 </context>
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
-        <translation type="unfinished"></translation>
+        <translation>Onbekend berichttype</translation>
     </message>
 </context>
 </TS>
diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts
index 72e4e771e398e2ae15068c4e19172a2ce43f4af4..0f625d446bb029d621ce6e897dc363db072660e6 100644
--- a/resources/langs/nheko_pl.ts
+++ b/resources/langs/nheko_pl.ts
@@ -4,25 +4,25 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
-        <translation type="unfinished"></translation>
+        <translation></translation>
     </message>
     <message>
         <location line="+10"/>
         <location line="+10"/>
         <source>Connecting...</source>
-        <translation type="unfinished"></translation>
+        <translation>Łączenie...</translation>
     </message>
     <message>
         <location line="+67"/>
         <source>You are screen sharing</source>
-        <translation type="unfinished"></translation>
+        <translation>Udostępniasz ekran</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Hide/Show Picture-in-Picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Ukryj/Pokaż Obraz w obrazie</translation>
     </message>
     <message>
         <location line="+13"/>
@@ -56,120 +56,120 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Połączenie Wideo</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Voice Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Połączenie Głosowe</translation>
     </message>
     <message>
         <location line="+62"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie wykryto mikrofonu.</translation>
     </message>
 </context>
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Połączenie Wideo</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Voice Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Połączenie Głosowe</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Devices</source>
-        <translation type="unfinished">UrzÄ…dzenia</translation>
+        <translation>UrzÄ…dzenia</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Accept</source>
-        <translation type="unfinished">Akceptuj</translation>
+        <translation>Akceptuj</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Unknown microphone: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Nieznany mikrofon: %1</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Unknown camera: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Nieznana kamera: %1</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Decline</source>
-        <translation type="unfinished">Odrzuć</translation>
+        <translation>Odrzuć</translation>
     </message>
     <message>
         <location line="-28"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie znaleziono mikrofonu.</translation>
     </message>
 </context>
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Cały ekran</translation>
     </message>
 </context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Nie udało się zaprosić użytkownika: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Zaproszono użytkownika %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
-        <translation type="unfinished"></translation>
+        <translation>Migracja cachu do obecnej wersji nieudana. Przyczyny mogą być różne. Proszę zgłosić błąd i w miedzyczasie używać starszej wersji. Możesz również spróbuwać usunąć cache ręcznie.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
-        <translation type="unfinished"></translation>
+        <translation>Potwierdź dołączenie</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to join %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Czy na pewno chcesz dołączyć&#xa0;do %1?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
-        <translation type="unfinished"></translation>
+        <translation>Utworzono pokój %1.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Potwierdź zaproszenie</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Czy na pewno chcesz zaprosić %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Zaproszenie %1 do %2 nieudane: %3</translation>
     </message>
     <message>
         <location line="+15"/>
@@ -182,7 +182,7 @@
         <translation>czy na pewno chcesz wykopać %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Wykopano użytkownika: %1</translation>
     </message>
@@ -197,9 +197,9 @@
         <translation>Czy na pewno chcesz zablokować %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie udało się zbanować %1 w %2: %3</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -217,9 +217,9 @@
         <translation>Czy na pewno chcesz odblokować %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie udało się odbanować %1 w %2: %3</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -227,12 +227,12 @@
         <translation>Odblokowano użytkownika: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Czy na pewno chcesz rozpocząć prywatny czat z %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Nie udało się przenieść pamięci podręcznej!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>Pamięć podręczna na Twoim dysku jest nowsza niż wersja obsługiwana przez Nheko. Zaktualizuj lub wyczyść pamięć podręczną.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <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>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Nie udało się ustawić kluczy szyfrujących. Odpowiedź serwera: %1 %2. Spróbuj ponownie później.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Spróbuj zalogować się ponownie: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Nie udało się dołączyć do pokoju: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Dołączyłeś do pokoju</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Nie udało się usunąć zaproszenia: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Tworzenie pokoju nie powiodło się: %1</translation>
     </message>
@@ -293,9 +295,9 @@
         <translation>Nie udało się opuścić pokoju: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie udało się wykopać %1 z %2: %3</translation>
     </message>
 </context>
 <context>
@@ -303,7 +305,7 @@
     <message>
         <location filename="../qml/CommunitiesList.qml" line="+44"/>
         <source>Hide rooms with this tag or from this space by default.</source>
-        <translation type="unfinished"></translation>
+        <translation>Domyślnie ukryj pokoje oznaczone tym tagiem z tej przestrzeni.</translation>
     </message>
 </context>
 <context>
@@ -311,70 +313,70 @@
     <message>
         <location filename="../../src/timeline/CommunitiesModel.cpp" line="+37"/>
         <source>All rooms</source>
-        <translation type="unfinished">Wszystkie pokoje</translation>
+        <translation>Wszystkie pokoje</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Shows all rooms without filtering.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokazuj wszystkie pokoje bez filtrowania.</translation>
     </message>
     <message>
         <location line="+30"/>
         <source>Favourites</source>
-        <translation type="unfinished"></translation>
+        <translation>Ulubione</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms you have favourited.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokoje dodane przez ciebie do ulubionych.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Low Priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Niski Priorytet</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms with low priority.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokoje o niskim priorytecie.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Server Notices</source>
-        <translation type="unfinished">Ogłoszenia serwera</translation>
+        <translation>Ogłoszenia Serwera</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Messages from your server or administrator.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wiadomości od twojego serwera lub administratora.</translation>
     </message>
 </context>
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
-        <translation type="unfinished"></translation>
+        <translation>Odszyfruj sekrety</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Enter your recovery key or passphrase to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Wprowadź swój klucz odzyskiwania lub frazę-klucz by odszyfrować swoje sekrety:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Wprowadź swój klucz odzyskiwania lub frazę klucz nazwaną: %1 by odszyfrować swoje sekrety:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Odszyfrowywanie nieudane</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Failed to decrypt secrets with the provided recovery key or passphrase</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie udało się odszyfrować sekretów przy pomocy podanego klucza odzyskiwania albo frazy-klucz</translation>
     </message>
 </context>
 <context>
@@ -431,7 +433,7 @@
         <translation>Szukaj</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Ludzie</translation>
     </message>
@@ -448,7 +450,7 @@
     <message>
         <location line="+2"/>
         <source>Activity</source>
-        <translation type="unfinished">Aktywność</translation>
+        <translation>Aktywność</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -458,17 +460,17 @@
     <message>
         <location line="+2"/>
         <source>Objects</source>
-        <translation type="unfinished">Przedmioty</translation>
+        <translation>Przedmioty</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Symbols</source>
-        <translation type="unfinished">Symbole</translation>
+        <translation>Symbole</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Flags</source>
-        <translation type="unfinished">Flagi</translation>
+        <translation>Flagi</translation>
     </message>
 </context>
 <context>
@@ -495,71 +497,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Ta wiadomość nie jest zaszyfrowana!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Brakuje klucza do odblokowania tej wiadomości. Poprosiliśmy o klucz automatycznie, ale możesz poprosić ręcznie jeszcze raz, jeśli jesteś niecierpliwy(a).</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Ta wiadomość nie mogła zostać odszyfrowana, ponieważ mamy klucz wyłącznie dla nowszych wiadomości. Możesz spróbować poprosić o dostęp do tej wiadomości.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation type="unfinished"></translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Wystąpił wewnętrzny błąd podczas próby odczytu klucza do odszyfrowywania z bazy danych.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation type="unfinished"></translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Wystąpił błąd podczas odszyfrowywania tej wiadomości.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>-- Zdarzenie szyfrowania (Nie znaleziono kluczy deszyfrujÄ…cych)</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Wystąpił błąd podczas przetwarzania tej wiadomości.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>Ten klucz szyfrowania został już użyty! Być może ktoś próbuje umieścić fałszywe wiadomości w tym czacie!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>--Błąd deszyfrowania (nie udało się uzyskać kluczy megolm z bazy danych) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Niezidentyfikowany błąd odszyfrowywania</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Błąd Deszyfracji (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>PoproÅ› o klucz</translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>-- Zdarzenie szyfrowania (Nieznany typ zdarzenia) --</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Ta wiadomość nie jest zaszyfrowana!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Atak powtórzeniowy! Indeks tej wiadomości został użyty ponownie! --</translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Zaszyfrowane przez zweryfikowane urzÄ…dzenie</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Wiadomość z niezweryfikowanego urządzenia! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Zaszyfrowane przez niezweryfikowane urządzenie, ale pochodzące od zaufanego użytkownika.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Zaszyfrowane przez niezweryfikowane urządzenie, albo klucz pochodzi z niezaufanego źródła, np. backup-u klucza.</translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Przekroczono limit czasu na weryfikacjÄ™ urzÄ…dzenia.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Druga strona anulowała weryfikację.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Zamknij</translation>
     </message>
@@ -601,7 +610,82 @@
     <message>
         <location filename="../qml/ForwardCompleter.qml" line="+44"/>
         <source>Forward Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Prześlij wiadomość dalej</translation>
+    </message>
+</context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>Edytowanie paczki obrazów</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Dodaj obrazy</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Naklejki (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation>Unikalny klucz paczki</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Nazwa paczki</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation>Źródło (autor/link)</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Użyj jako Emoji</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Użyj jako Naklejki</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation>Skrót</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation>Treść</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Usuń z paczki</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Usuń</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Anuluj</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Zapisz</translation>
     </message>
 </context>
 <context>
@@ -609,89 +693,130 @@
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustawienia paczki obrazów</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Utwórz paczkę konta</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Nowa paczka pokoju</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Prywatna paczka</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Paczka z tego pokoju</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Paczka włączona globalnie</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Włącz globalnie</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Umożliw używanie tej paczki we wszystkich pokojach</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Edytuj</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished">Zamknij</translation>
+        <translation>Zamknij</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
-        <translation type="unfinished">Wybierz plik</translation>
+        <translation>Wybierz plik</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Wszystkie pliki (*)</translation>
+        <translation>Wszystkie pliki (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłanie mediów nie powiodło się. Spróbuj ponownie.</translation>
     </message>
 </context>
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Zaproś użytkowników do %1</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>User ID to invite</source>
-        <translation type="unfinished">ID użytkownika do zaproszenia</translation>
+        <translation>ID użytkownika do zaproszenia</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@ania:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>Dodaj</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>ZaproÅ›</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Anuluj</translation>
+        <translation>Anuluj</translation>
+    </message>
+</context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">ID pokoju lub alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Opuść pokój</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Czy na pewno chcesz wyjść?</translation>
     </message>
 </context>
 <context>
@@ -724,7 +849,7 @@ Jeżeli Nheko nie odnajdzie Twojego serwera domowego, wyświetli formularz umoż
     <message>
         <location line="+2"/>
         <source>Your password.</source>
-        <translation type="unfinished"></translation>
+        <translation>Twoje hasło.</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -739,18 +864,19 @@ Jeżeli Nheko nie odnajdzie Twojego serwera domowego, wyświetli formularz umoż
     <message>
         <location line="+4"/>
         <source>Homeserver address</source>
-        <translation type="unfinished"></translation>
+        <translation>Adres Homeserwer-a</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>server.my:8787</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The address that can be used to contact you homeservers client API.
 Example: https://server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>Adres który może być użyty do komunikacji z klienckim API homeserwer-a.
+Przykład: https://server.my:8787</translation>
     </message>
     <message>
         <location line="+19"/>
@@ -758,25 +884,25 @@ Example: https://server.my:8787</source>
         <translation>ZALOGUJ</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
-        <translation type="unfinished"></translation>
+        <translation>Wprowadzono nieprawidłowe Matrix ID. Przykład prawidłowego ID: @ania:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Automatyczne odkrywanie zakończone niepowodzeniem. Otrzymano nieprawidłową odpowiedź.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Automatyczne odkrywanie zakończone niepowodzeniem. Napotkano nieznany błąd. .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Nie odnaleziono wymaganych punktów końcowych. To może nie być serwer Matriksa.</translation>
     </message>
@@ -786,33 +912,51 @@ Example: https://server.my:8787</source>
         <translation>Otrzymano nieprawidłową odpowiedź. Upewnij się, że domena serwera domowego jest prawidłowa.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Wystąpił nieznany błąd. Upewnij się, że domena serwera domowego jest prawidłowa.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>Logowanie SSO</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Puste hasło</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>Logowanie SSO zakończone niepowodzeniem</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
-        <translation type="unfinished"></translation>
+        <translation>usunięto</translation>
     </message>
     <message>
         <location line="+9"/>
@@ -820,7 +964,7 @@ Example: https://server.my:8787</source>
         <translation>Szyfrowanie włączone</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>Nazwa pokoju zmieniona na: %1</translation>
     </message>
@@ -842,7 +986,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+12"/>
         <source>%1 changed the room avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zmienił avatar pokoju</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -857,7 +1001,7 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+2"/>
         <source>%1 placed a video call.</source>
-        <translation>%1 rozpoczął(-ęła) połączenie wideo</translation>
+        <translation>%1 rozpoczął(-ęła) połączenie wideo.</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -879,141 +1023,153 @@ Example: https://server.my:8787</source>
         <source>Negotiating call...</source>
         <translation>Negocjowanie połączenia…</translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Wpuść</translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
     <message>
         <location filename="../qml/MessageInput.qml" line="+44"/>
         <source>Hang up</source>
-        <translation type="unfinished"></translation>
+        <translation>Rozłącz się</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Place a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Zadzwoń</translation>
     </message>
     <message>
         <location line="+25"/>
         <source>Send a file</source>
-        <translation type="unfinished">Wyślij plik</translation>
+        <translation>Wyślij plik</translation>
     </message>
     <message>
         <location line="+50"/>
         <source>Write a message...</source>
-        <translation type="unfinished">Napisz wiadomość…</translation>
+        <translation>Napisz wiadomość…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>Naklejki</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>Emoji</source>
-        <translation type="unfinished">Emoji</translation>
+        <translation>Emoji</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Send</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyślij</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>You don&apos;t have permission to send messages in this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie masz uprawnień do wysyłania wiadomości w tym pokoju</translation>
     </message>
 </context>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>Edytuj</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>React</source>
-        <translation type="unfinished"></translation>
+        <translation>Zareaguj</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Reply</source>
-        <translation type="unfinished"></translation>
+        <translation>Odpisz</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Options</source>
-        <translation type="unfinished"></translation>
+        <translation>Opcje</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Kopiuj</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
-        <translation type="unfinished"></translation>
+        <translation>Kopiuj &amp;adres odnośnika</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
-        <translation type="unfinished"></translation>
+        <translation>Zar&amp;eaguj</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Repl&amp;y</source>
-        <translation type="unfinished"></translation>
+        <translation>Odp&amp;isz</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Edytuj</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Read receip&amp;ts</source>
-        <translation type="unfinished"></translation>
+        <translation>Sprawdź &amp;odbiorców</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>&amp;Forward</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Przekaż dalej</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>&amp;Mark as read</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Oznacz jako przeczytane</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>View raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyświetl nowe wiadomości</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>View decrypted raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyświetl odszyfrowaną nową wiadomość</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Remo&amp;ve message</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Usuń wiadomość</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Save as</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Zapisz jako</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Open in external program</source>
-        <translation type="unfinished"></translation>
+        <translation>Otwórz w &amp;zewnętrznym programie</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Copy link to eve&amp;nt</source>
-        <translation type="unfinished"></translation>
+        <translation>Skopiuj link do z&amp;darzenia</translation>
+    </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>Idź do zacytowanej wiado&amp;mości</translation>
     </message>
 </context>
 <context>
@@ -1021,37 +1177,42 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/NewVerificationRequest.qml" line="+11"/>
         <source>Send Verification Request</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyślij prośbę o weryfikację</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Received Verification Request</source>
+        <translation>Otrzymano prośbę o weryfikację</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
-        <translation type="unfinished"></translation>
+        <translation>Aby umośliwić innym użytkownikom identyfikację, które urządzenia faktycznie należą do Ciebie, możesz wykonać ich weryfikację. To również umożliwi automatyczny backup kluczy. Zweryfikować %1 teraz?</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.</source>
-        <translation type="unfinished"></translation>
+        <translation>Aby upewnić się, że nikt nie podsłuchuje twojej komunikacji możesz wykonać proces weryfikacji rozmówcy.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 has requested to verify their device %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 poprosił Cię o weryfikację jego/jej urządzenia: %2.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 using the device %2 has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>Użytkownik %1 poprosił Cię o weryfikację swojego urządzenia: %2.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your device (%1) has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>Twoje urządzenie (%1) poprosiło o weryfikację.</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -1075,47 +1236,44 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
+        <translation>%1 wysłał(a) zaszyfrowaną wiadomość</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
+        <translatorcomment>Format wiadomości w powiadomieniu. %1 to nadawca, %2 to wiadomość.</translatorcomment>
+        <translation>%1 odpisał(a): %2</translation>
     </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
         <source>%1 replied with an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 odpisał(a) zaszyfrowaną wiadomością</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>%1 replied to a message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 odpisał(a) na wiadomość</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>%1 sent a message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) wiadomość</translation>
     </message>
 </context>
 <context>
@@ -1123,32 +1281,32 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/PlaceCall.qml" line="+48"/>
         <source>Place a call to %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Rozpocząć połączenie głosowe z %1?</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie znaleziono mikrofonu.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
-        <translation type="unfinished"></translation>
+        <translation>Dźwięk</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Video</source>
-        <translation type="unfinished"></translation>
+        <translation>Wideo</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Ekran</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Cancel</source>
-        <translation type="unfinished">Anuluj</translation>
+        <translation>Anuluj</translation>
     </message>
 </context>
 <context>
@@ -1162,7 +1320,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation>Stwórz unikalny profil, który pozwoli Ci na zalogowanie się do kilku kont jednocześnie i uruchomienie wielu instancji Nheko.</translation>
     </message>
@@ -1177,21 +1335,37 @@ Example: https://server.my:8787</source>
         <translation>nazwa profilu</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Potwierdzenia przeczytania</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Wczoraj, %1</translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Nazwa użytkownika</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Nazwa użytkownika nie może być pusta i  może  zawierać wyłącznie znaki a-z, 0-9, ., _, =, -, i /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Hasło</translation>
     </message>
@@ -1211,7 +1385,7 @@ Example: https://server.my:8787</source>
         <translation>Serwer domowy</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Serwer, który pozwala na rejestrację. Ponieważ Matrix jest zdecentralizowany, musisz najpierw znaleźć serwer który pozwala na rejestrację bądź hostować swój własny.</translation>
     </message>
@@ -1221,52 +1395,42 @@ Example: https://server.my:8787</source>
         <translation>ZAREJESTRUJ</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Nie wspierana procedura rejestracji!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished">Automatyczne odkrywanie zakończone niepowodzeniem. Otrzymano nieprawidłową odpowiedź.</translation>
+        <translation>Automatyczne odkrywanie zakończone niepowodzeniem. Otrzymano nieprawidłową odpowiedź.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished">Automatyczne odkrywanie zakończone niepowodzeniem. Napotkano nieznany błąd. .well-known.</translation>
+        <translation>Automatyczne odkrywanie zakończone niepowodzeniem. Napotkano nieznany błąd. .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished">Nie odnaleziono wymaganych punktów końcowych. To może nie być serwer Matriksa.</translation>
+        <translation>Nie odnaleziono wymaganych interfejsów. To może nie być serwer Matrix.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished">Otrzymano nieprawidłową odpowiedź. Upewnij się, że domena serwera domowego jest prawidłowa.</translation>
+        <translation>Otrzymano nieprawidłową odpowiedź. Upewnij się, że domena homeserver-a jest prawidłowa.</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished">Wystąpił nieznany błąd. Upewnij się, że domena serwera domowego jest prawidłowa.</translation>
+        <translation>Wystąpił nieznany błąd. Upewnij się, że domena homeserver-a jest prawidłowa.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Hasło jest zbyt krótkie (min. 8 znaków)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Hasła nie pasują do siebie</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Nieprawidłowa nazwa serwera</translation>
     </message>
@@ -1274,295 +1438,390 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Zamknij</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Cancel edit</source>
+        <translation>Anuluj edytowanie</translation>
+    </message>
+</context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>PrzeglÄ…daj Pokoje Publiczne</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Szukaj publicznych pokojów</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
-        <translation type="unfinished"></translation>
+        <translation>wersja nie została zachowana</translation>
     </message>
 </context>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
-        <translation type="unfinished"></translation>
+        <translation>Nowy tag</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter the tag you want to use:</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
+        <translation>Wprowadź tag, którego chcesz użyć:</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
-        <translation type="unfinished">Opuść pokój</translation>
+        <translation>Opuść pokój</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Tag room as:</source>
-        <translation type="unfinished"></translation>
+        <translation>Oznacz (tag) pokój jako:</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Favourite</source>
-        <translation type="unfinished"></translation>
+        <translation>Ulubione</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Low priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Niski priorytet</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Server notice</source>
-        <translation type="unfinished"></translation>
+        <translation>Powiadomienie serwera</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Create new tag...</source>
-        <translation type="unfinished"></translation>
+        <translation>Utwórz nowy tag...</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Wiadomość Statusowa</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter your status message:</source>
-        <translation type="unfinished"></translation>
+        <translation>Wprowadź swoją wiadomość statusową:</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Profile settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustawienia profilu</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Set status message</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustaw wiadomość statusową</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
-        <translation type="unfinished">Wyloguj</translation>
+        <translation>Wyloguj</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Zamknij</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
-        <translation type="unfinished">Utwórz nowy czat</translation>
+        <translation>Utwórz nowy czat</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Join a room</source>
-        <translation type="unfinished">Dołącz do pokoju</translation>
+        <translation>Dołącz do pokoju</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Create a new room</source>
-        <translation type="unfinished"></translation>
+        <translation>Utwórz nowy pokój</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Room directory</source>
-        <translation type="unfinished">Katalog pokojów</translation>
+        <translation>Katalog pokojów</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
-        <translation type="unfinished">Ustawienia użytkownika</translation>
+        <translation>Ustawienia użytkownika</translation>
     </message>
 </context>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Obecni w %1</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%n osoba w %1</numerusform>
+            <numerusform>%n osób w %1</numerusform>
+            <numerusform>%n osób w %1</numerusform>
         </translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Invite more people</source>
-        <translation type="unfinished"></translation>
+        <translation>Zaproś więcej ludzi</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ten pokój jest szyfrowany!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Ten użytkownik został zweryfikowany.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Ten użytkownik nie został zweryfikowany, ale wciąż używa tego samego klucza głównego którego używał podczas waszej pierwszej rozmowy.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Ten użytkownik ma urządzenie, które nie zostały zweryfikowane!</translation>
     </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustawienia Pokoju</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 użytkownik(ów)</translation>
     </message>
     <message>
         <location line="+55"/>
         <source>SETTINGS</source>
-        <translation type="unfinished"></translation>
+        <translation>USTAWIENIA</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Notifications</source>
-        <translation type="unfinished"></translation>
+        <translation>Powiadomienia</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Muted</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyciszony</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Mentions only</source>
-        <translation type="unfinished"></translation>
+        <translation>Tylko wzmianki</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All messages</source>
-        <translation type="unfinished"></translation>
+        <translation>Wszystkie wiadomości</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Dostęp do pokoju</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
-        <translation type="unfinished"></translation>
+        <translation>Każdy oraz goście</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Anyone</source>
-        <translation type="unfinished"></translation>
+        <translation>Każdy</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Invited users</source>
-        <translation type="unfinished"></translation>
+        <translation>Zaproszeni użytkownicy</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>PukajÄ…c</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Zastrzeżone poprzez członkostwo w innych pokojach</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
-        <translation type="unfinished"></translation>
+        <translation>Szyfrowanie</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>End-to-End Encryption</source>
-        <translation type="unfinished">Szyfrowanie end-to-end</translation>
+        <translation>Szyfrowanie end-to-end</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
                             Please take note that it can&apos;t be disabled afterwards.</source>
-        <translation type="unfinished"></translation>
+        <translation>Szyfrowanie w chwili obecnej jest eksperymentalne i może popsuć się w dowolnym momencie.&lt;br&gt;
+                                 Proszę pamiętaj, że szyfrowanie nie będzie mogło zostać później wyłączone.</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Naklejki i Ustawienia Emote</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Zmień</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Wybież, które paczki są włączone, usuń paczki, lub utwórz nowe</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>INFO</source>
-        <translation type="unfinished"></translation>
+        <translation>INFO</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Internal ID</source>
-        <translation type="unfinished"></translation>
+        <translation>Wewnętrzne ID</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Room Version</source>
-        <translation type="unfinished"></translation>
+        <translation>Wersja Pokoju</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
-        <translation type="unfinished">Nie udało się włączyć szyfrowania: %1</translation>
+        <translation>Nie udało się włączyć szyfrowania: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
-        <translation type="unfinished">Wybierz awatar</translation>
+        <translation>Wybierz awatar</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Wszystkie pliki (*)</translation>
+        <translation>Wszystkie pliki (*)</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>The selected file is not an image</source>
-        <translation type="unfinished"></translation>
+        <translation>Wybrany plik nie jest obrazem</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Error while reading file: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Błąd czytania pliku: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
-        <translation type="unfinished">Nie udało się wysłać obrazu: %s</translation>
+        <translation>Nie udało się wysłać obrazu: %s</translation>
     </message>
 </context>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>OczekujÄ…ce zaproszenie.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
-        <translation type="unfinished"></translation>
+        <translation>PodglÄ…d tego pokoju</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
+        <translation>Podgląd pokoju niedostępny</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1571,53 +1830,168 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/ScreenShare.qml" line="+30"/>
         <source>Share desktop with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Udostępnić pulpit (desktop) użytkownikowi: %1?</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Window:</source>
-        <translation type="unfinished"></translation>
+        <translation>Okno:</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Frame rate:</source>
-        <translation type="unfinished"></translation>
+        <translation>Klatek na sekundÄ™:</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Include your camera picture-in-picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Włącz funkcję picture-in-picture kamery</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Request remote camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Poproś rozmówcę o włączenie kamery</translation>
     </message>
     <message>
         <location line="+1"/>
         <location line="+9"/>
         <source>View your callee&apos;s camera like a regular video call</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyświetl widok kamery rozmówcy jak podczas zwykłej rozmoowy wideo</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Hide mouse cursor</source>
-        <translation type="unfinished"></translation>
+        <translation>Ukryj kursor myszy</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Share</source>
-        <translation type="unfinished"></translation>
+        <translation>Udostępnij</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Preview</source>
-        <translation type="unfinished"></translation>
+        <translation>PodglÄ…d</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished">Anuluj</translation>
+        <translation>Anuluj</translation>
+    </message>
+</context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Błąd połączenia do menadżera sekretów</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Nie udało się uaktualnić paczki obrazów: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Nie udało się usunąć starej paczki obrazów: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Nie udało się otworzyć obrazu: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Nie udało się wysłać obrazu: %1</translation>
     </message>
 </context>
 <context>
@@ -1625,22 +1999,22 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/StatusIndicator.qml" line="+24"/>
         <source>Failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Błąd</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Sent</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłano</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Received</source>
-        <translation type="unfinished"></translation>
+        <translation>Otrzymano</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Read</source>
-        <translation type="unfinished"></translation>
+        <translation>Przeczytano</translation>
     </message>
 </context>
 <context>
@@ -1648,7 +2022,7 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
         <source>Search</source>
-        <translation type="unfinished">Szukaj</translation>
+        <translation>Szukaj</translation>
     </message>
 </context>
 <context>
@@ -1656,274 +2030,306 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/Success.qml" line="+11"/>
         <source>Successful Verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Weryfikacja udana</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Verification successful! Both sides verified their devices!</source>
-        <translation type="unfinished"></translation>
+        <translation>Weryfikacja udana! Obaj rozmówcy zweryfikowali swoje urządzenia!</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Close</source>
-        <translation type="unfinished">Zamknij</translation>
+        <translation>Zamknij</translation>
     </message>
 </context>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
-        <translation type="unfinished">Redagowanie wiadomości nie powiodło się: %1</translation>
+        <translation>Cenzurowanie wiadomości nie powiodło się: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
-        <translation type="unfinished"></translation>
+        <translation>Szyfrowanie event-u nie powiodło się, wysyłanie anulowane!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
-        <translation type="unfinished">Zapisz obraz</translation>
+        <translation>Zapisz obraz</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save video</source>
-        <translation type="unfinished"></translation>
+        <translation>Zapisz wideo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save audio</source>
-        <translation type="unfinished"></translation>
+        <translation>Zapisz audio</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save file</source>
-        <translation type="unfinished"></translation>
+        <translation>Zapisz plik</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%1 %2 pisze.</numerusform>
+            <numerusform>%1, oraz %2 piszą (w sumie %n osób).</numerusform>
+            <numerusform>%1, oraz %2 piszą (w sumie %n osób).</numerusform>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zmienił(a) status pokoju na publiczny.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 made this room require and invitation to join.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 oznaczył(a) pokój jako wymagający zaproszenia aby do niego dołączyć.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 zazwolił(a) na dołączenie do tego pokoju poprzez pukanie.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 zezwolił(a) członkom następujących pokojów na automatyczne dołączenie do tego pokoju: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 pozwoliła na dostęp gościom do tego pokoju.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 has closed the room to guest access.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zabronił(a) gościom dostępu do tego pokoju.</translation>
     </message>
     <message>
         <location line="+23"/>
         <source>%1 made the room history world readable. Events may be now read by non-joined people.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zezwoliła każdemu na dostęp do historii tego pokoju. Eventy mogą teraz zostać przeczytane przez osoby nie będące członkami tego pokoju.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>%1 set the room history visible to members from this point on.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 umożliwił członkom tego pokoju na dostęp od teraz.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 umożliwił dostęp do historii tego pokoju członkom od momentu kiedy zostali zaproszeni.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 umożliwił dostęp do historii tego pokoju członkom od momentu kiedy dołączyli do pokoju.</translation>
     </message>
     <message>
         <location line="+22"/>
         <source>%1 has changed the room&apos;s permissions.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zmienił(a) uprawnienia pokoju.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 został(a) zaproszona/y.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zmienił(a) swój awatar.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 changed some profile info.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zmodyfikował(a) dane profilu.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 dołączył(a).</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 dołączyła dzięki autoryzacji serwera użytkownika %2.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 odrzucił(a) zaproszenie.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Revoked the invite to %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Unieważniono zaproszenie dla %1.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 left the room.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 opuścił(a) pokój.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Kicked %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wykopano użytkownika %1.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Unbanned %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Odbanowano użytkownika %1.</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>%1 was banned.</source>
-        <translation type="unfinished"></translation>
+        <translation>Użytkownik %1 został zbanowany.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Powód: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
-        <translation type="unfinished"></translation>
+        <translation>Użytkownik %1 ocenzurował własne pukanie.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
-        <translation type="unfinished">Dołączyłeś(-łaś) do tego pokoju.</translation>
+        <translation>Dołączyłeś(-łaś) do tego pokoju.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>Użytkownik %1 zmienił swojego awatara i zmienił swoją nazwę wyświetlaną na %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>Użytkownik %1 zmienił swoją nazwę wyświetlaną na %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Odrzucono pukanie użytkownika %1.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 left after having already left!</source>
         <comment>This is a leave event after the user already left and shouldn&apos;t happen apart from state resets</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 opuścił(a) pokój po raz kolejny!</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>%1 knocked.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zapukał(a).</translation>
     </message>
 </context>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
-        <translation type="unfinished"></translation>
+        <translation>Edytowane</translation>
     </message>
 </context>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
-        <translation type="unfinished"></translation>
+        <translation>Brak otwartych pokojów</translation>
+    </message>
+    <message>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Podgląd pokoju niedostępny</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+7"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 obeczny(ch)</translation>
     </message>
     <message>
         <location line="+33"/>
         <source>join the conversation</source>
-        <translation type="unfinished"></translation>
+        <translation>Dołącz do rozmowy</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>accept invite</source>
-        <translation type="unfinished"></translation>
+        <translation>zaakceptój zaproszenie</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>decline invite</source>
-        <translation type="unfinished"></translation>
+        <translation>odrzuć zaproszenie</translation>
     </message>
     <message>
         <location line="+27"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wróc do listy pokoi</translation>
     </message>
 </context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
+        <translation>Wróć do listy pokoi</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
+        <translation>Nie wybrano pokoju</translation>
+    </message>
+    <message>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Ten pokój nie jest szyfrowany!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Ten pokój zawiera wyłącznie zweryfikowane urządzenia.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Ten pokój zawiera niezweryfikowane urządzenia!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
-        <translation type="unfinished">Ustawienia pokoju</translation>
+        <translation>Ustawienia pokoju</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Invite users</source>
-        <translation type="unfinished">Zaproś użytkowników</translation>
+        <translation>Zaproś użytkowników</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Members</source>
-        <translation type="unfinished">Członkowie</translation>
+        <translation>Członkowie</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -1949,78 +2355,168 @@ Example: https://server.my:8787</source>
         <translation>Zakończ</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Proszę wprowadzić prawidłowy token rejestracji.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
-        <translation type="unfinished"></translation>
+        <translation>Globalny Profil Użytkownika</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Room User Profile</source>
+        <translation>Profil Użytkownika Pokoju</translation>
+    </message>
+    <message>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Zmień awatar globalnie.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Zmień awatar wyłącznie dla bieżącego pokoju.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Zmień nazwę wyświetlaną globalnie.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Zmień nazwę wyświetlaną wyłącznie dla bieżącego pokoju.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Pokój: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>To profil specyficzny dla pokoju. Nazwa użytkownika oraz awatar mogą być inne niż globalna nazwa użytkownika i globalny awatar.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Otwórz globalny profil tego użytkownika.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation>Zweryfikuj</translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Rozpocznij prywatny czat.</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Wykop użytkownika.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Zbanuj użytkownika.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+31"/>
+        <source>Change device name.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation type="unfinished"></translation>
+        <location line="+27"/>
+        <source>Unverify</source>
+        <translation>Udweryfikuj</translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
-        <source>Unverify</source>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location line="+223"/>
         <source>Select an avatar</source>
-        <translation type="unfinished">Wybierz awatar</translation>
+        <translation>Wybierz awatar</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Wszystkie pliki (*)</translation>
+        <translation>Wszystkie pliki (*)</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>The selected file is not an image</source>
-        <translation type="unfinished"></translation>
+        <translation>Wybrany plik nie jest obrazem</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Error while reading file: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Błąd odczytu pliku: %1</translation>
     </message>
 </context>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Domyślne</translation>
     </message>
 </context>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Zminimalizuj do paska zadań</translation>
     </message>
@@ -2030,29 +2526,29 @@ Example: https://server.my:8787</source>
         <translation>Rozpocznij na pasku zadań</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Pasek boczny grupy</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
-        <translation type="unfinished"></translation>
+        <translation>Okrągłe awatary</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>profil: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Domyślny</translation>
     </message>
     <message>
         <location line="+31"/>
         <source>CALLS</source>
-        <translation type="unfinished"></translation>
+        <translation>POŁĄCZENIA</translation>
     </message>
     <message>
         <location line="+46"/>
@@ -2062,87 +2558,101 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+4"/>
         <source>REQUEST</source>
-        <translation type="unfinished"></translation>
+        <translation>POPROÅš O</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>DOWNLOAD</source>
-        <translation type="unfinished"></translation>
+        <translation>POBIERZ</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pozostaw aplikację działającą w tle po zamknięciu okna.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Start the application in the background without showing the client window.</source>
-        <translation type="unfinished"></translation>
+        <translation>Uruchamiaj aplikację w tle bez wyświetlania okna głównego.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change the appearance of user avatars in chats.
 OFF - square, ON - Circle.</source>
+        <translation>Zmień wygląd awatarów w czasie.
+OFF - kwadrat, ON - koło.</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokazuj kolumnÄ™ zawierajÄ…cÄ… grupy i tagi obok listy pokoi.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Decrypt messages in sidebar</source>
-        <translation type="unfinished"></translation>
+        <translation>Odszyfruj wiadomości na pasku bocznym</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Decrypt the messages shown in the sidebar.
 Only affects messages in encrypted chats.</source>
-        <translation type="unfinished"></translation>
+        <translation>Odszyfruj wiadomości na pasku bocznym.
+Dotyczy wyłącznie czatów z włączonym szyfrowaniem.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Privacy Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Ekran prywatności</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>When the window loses focus, the timeline will
 be blurred.</source>
-        <translation type="unfinished"></translation>
+        <translation>Kiedy okno traci fokus, historia zostanie rozmyta.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
-        <translation type="unfinished"></translation>
+        <translation>Opóźnienie ekranu prywatności (w sekundach [0 - 3600])</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set timeout (in seconds) for how long after window loses
 focus before the screen will be blurred.
 Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds)</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustaw czas (w sekundach) po którym okno zostanie rozmyte po
+stracie fokusu.
+Ustaw na 3 aby rozmywać natychmiast po stracie fokusu. Maksymalna wartość to 1 godz (3600 sekund)</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Show buttons in timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokazuj przyciski w historii</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show buttons to quickly reply, react or access additional options next to each message.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokazuj przyciski do reakcji albo dostępu do dodatkowych opcji obok każdej wiadomości.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Limit width of timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Ogranicz szerokość historii</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustaw maksymalną szerokość&#xa0;wiadomości w historii (w pikselach). Może to poprawić czytelność gdy Nheko zostanie zmaksymalizowany na szerokim ekranie</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2153,22 +2663,24 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds
         <location line="+2"/>
         <source>Show who is typing in a room.
 This will also enable or disable sending typing notifications to others.</source>
-        <translation type="unfinished"></translation>
+        <translation>To również włączy lub wyłączy wysyłanie powiadomień o pisaniu do innych.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Sort rooms by unreads</source>
-        <translation type="unfinished"></translation>
+        <translation>Sortuj pokoje po nieprzeczytanych wiadomościach</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Display rooms with new messages first.
 If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room.
 If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don&apos;t seem to consider them as important as the other rooms.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyświetlaj wiadomości z nieprzeczytanymi wiadomościami w pierwszej kolejności.
+Gdy ta opcja jest wyłączona, pokoje będą sortowane wyłącznie po znaczniku czasowym ostatniej wiadomości w pokoju.
+Gdy ta opcja jest włączona, pokoje z aktywnymi powiadomieniami (mało kółko z numerkiem w środku) będą na początku. Pokoje, które są wyciszone, będą sortowane po znaczniku czasowym, ponieważ zdaje się, że nie uważasz ich za równie wazne co pozostałe pokoje.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Potwierdzenia przeczytania</translation>
     </message>
@@ -2176,94 +2688,137 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <location line="+2"/>
         <source>Show if your message was read.
 Status is displayed next to timestamps.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokaż czy twoja wiadomość została przeczytana.
+Status jest wyświetlany obok znacznika czasu.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysyłaj wiadomości używając Markdown-u</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Allow using markdown in messages.
 When disabled, all messages are sent as a plain text.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pozwól na używanie markdown-u w wiadomościach.
+Gdy ta opcja jest wyłączona, wszystkie wiadomości będą wysyłane gołym tekstem.</translation>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation>Odtwarzaj animacje obrazów tylko gdy kursor myszy jest nad nimi</translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Powiadomienia na pulpicie</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Notify about received message when the client is not currently focused.</source>
-        <translation type="unfinished"></translation>
+        <translation>Powiadamiaj o odrzymanych wiadomościach gdy klient nie jest obecnie zfokusowany.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Alert on notification</source>
-        <translation type="unfinished"></translation>
+        <translation>Alert podczas notyfikacji</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show an alert when a message is received.
 This usually causes the application icon in the task bar to animate in some fashion.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokazuj alert gdy przychodzi wiadomość.
+To zwykle sprawia, że ikona aplikacji w tacce systemowej jest animowana.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Highlight message on hover</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyróżnij wiadomości gdy kursor myszy znajduje się nad nimi.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the background color of messages when you hover over them.</source>
-        <translation type="unfinished"></translation>
+        <translation>Zmień tło wiadomości kiedy kursor myszy znajduje się nad nimi.</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Large Emoji in timeline</source>
-        <translation type="unfinished"></translation>
+        <translation>Duże emotikony w historii</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Make font size larger if messages with only a few emojis are displayed.</source>
-        <translation type="unfinished"></translation>
+        <translation>Zwiększ rozmiar czcionki gdy wiadomości zawierają tylko kilka emotikon.</translation>
+    </message>
+    <message>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation>Wysyłaj zaszyfrowane wiadomości wyłącznie do zweryfikowanych użytkowników</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation>Wymaga zweryfikowania użytkownika zanim będzie możliwe wysłanie zaszyfrowanych wiadomości do niego. To zwiększa bezpieczeństwo, ale sprawia, że szyfrowanie E2E jest bardziej niewygodne w użyciu.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
-        <translation type="unfinished"></translation>
+        <translation>Udostępnij klucze zweryfikowanym użytkownikom i urządzeniom</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation>Automatycznie odpowiada na prośby o klucze od zweryfikowanych użytkowników, nawet gdy ci nie powinni mieć dostępu do tych kluczy.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation>Backup Kluczy Online</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation>Pobierz klucze szyfrowania wiadomości i umieść w szyfrowanym backup-ie kluczy online.</translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation>Włącz backup kluczy online</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation>Autorzy Nheko nie zalecają włączenia backup-u kluczy online dopóki symetryczny backup kluczy online nie jest dostępny. Włączyć mimo to?</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+253"/>
         <source>CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>ZAPISANE W CACHE-U</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>NOT CACHED</source>
-        <translation type="unfinished"></translation>
+        <translation>NIE ZAPISANE W CACHE-U</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
-        <translation type="unfinished"></translation>
+        <translation>Współczynnik skalowania</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Change the scale factor of the whole user interface.</source>
-        <translation type="unfinished"></translation>
+        <translation>Zmień współczynnik skalowania całego interfejsu użytkownika.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Font size</source>
-        <translation type="unfinished"></translation>
+        <translation>Wielkość czcionki</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Font Family</source>
-        <translation type="unfinished"></translation>
+        <translation>Rodzina czcionki</translation>
     </message>
     <message>
         <location line="+8"/>
@@ -2273,42 +2828,42 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+4"/>
         <source>Ringtone</source>
-        <translation type="unfinished"></translation>
+        <translation>Dzwonek</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set the notification sound to play when a call invite arrives</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustaw dźwięk powiadomienia odtwarzanego podczas zaproszenia do połączenia</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Microphone</source>
-        <translation type="unfinished"></translation>
+        <translation>Mikrofon</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Kamera</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera resolution</source>
-        <translation type="unfinished"></translation>
+        <translation>Rozdzielczość kamery</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Camera frame rate</source>
-        <translation type="unfinished"></translation>
+        <translation>Ilość klatek na sekundę kamery</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Allow fallback call assist server</source>
-        <translation type="unfinished"></translation>
+        <translation>Pozwól na korzystanie z serwera pomocniczego do nawiązywania połączeń głosowych/wideo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will use turn.matrix.org as assist when your home server does not offer one.</source>
-        <translation type="unfinished"></translation>
+        <translation>Używaj serwera pomocniczego turn.matrix.org gdy twój homeserver nie udostępnia własnego serwera pomocniczego.</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2321,19 +2876,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Odcisk palca urzÄ…dzenia</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Klucze sesji</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>IMPORT</source>
-        <translation type="unfinished"></translation>
+        <translation>IMPORTUJ</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>EXPORT</source>
-        <translation type="unfinished"></translation>
+        <translation>EKSPORTUS</translation>
     </message>
     <message>
         <location line="-34"/>
@@ -2341,126 +2896,134 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>SZYFROWANIE</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>OGÓLNE</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
-        <translation type="unfinished"></translation>
+        <translation>INTERFEJS</translation>
+    </message>
+    <message>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation>Odtwarzaj media takie jak GIF  czy WEBP tylko gdy wskazane przy użyciu myszy.</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
-        <translation type="unfinished"></translation>
+        <translation>Tryb ekranu dotykowego</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Will prevent text selection in the timeline to make touch scrolling easier.</source>
-        <translation type="unfinished"></translation>
+        <translation>Zapobiega zaznaczaniu tekstu w historii rozmów by ułatwić przewijanie przy użyciu interfejsu dotykowego.</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Emoji Font Family</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>Rodzina czcionki emotikon</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Główny klucz podpisywania</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your most important key. You don&apos;t need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys.</source>
-        <translation type="unfinished"></translation>
+        <translation>Twój najważniejszy klucz. Nie musi być zapisany w cache-u -- nie zapisując go w cache-u zmniejszasz ryzyko, że ten klucz zostanie wykradzony. Ponadto, ten klucz jest potrzebny wyłącznie podczas odświerzania kluczy podpisywania sesji.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>User signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Klucz podpisywania użytkownika</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify other users. If it is cached, verifying a user will verify all their devices.</source>
-        <translation type="unfinished"></translation>
+        <translation>Klucz używany do weryfikacji innych użytkowników. Gdy zapisany w cache-u, weryfikacja użytkownika dokona weryfikacji wszystkich jego urządzeń.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
-        <translation type="unfinished"></translation>
+        <translation>Klucz samo-podpisywania</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you.</source>
-        <translation type="unfinished"></translation>
+        <translation>Klucz służący do weryfikacji twoich własnych urządzeń. Jeśli zapisany w cache-u, weryfikacja jednego z twoich urządzeń sprawi, że będzie ono zweryfikowane dla wszystkich twoich pozostałych urządzeń oraz dla tych użytkowników, którzy zweryfikowali Ciebie.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Backup key</source>
-        <translation type="unfinished"></translation>
+        <translation>Klucz backup-u</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server.</source>
-        <translation type="unfinished"></translation>
+        <translation>Klucz służący do odszyfrowania backup-u kluczy online. Jeśli zapisany w cache-u, możesz włączyć backup kluczy online by zapisać w klucze zaszyfrowane bezpiecznie w backup-ie na serwerze.</translation>
     </message>
     <message>
         <location line="+54"/>
         <source>Select a file</source>
-        <translation type="unfinished">Wybierz plik</translation>
+        <translation>Wybierz plik</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished">Wszystkie pliki (*)</translation>
+        <translation>Wszystkie pliki (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
-        <translation type="unfinished"></translation>
+        <translation>Otwórz Plik Sesji</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
         <source>Error</source>
-        <translation type="unfinished"></translation>
+        <translation>Błąd</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
-        <translation type="unfinished"></translation>
+        <translation>Hasło Pliku</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
-        <translation type="unfinished"></translation>
+        <translation>Wpisz frazÄ™ do odszyfrowania pliku:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
-        <translation type="unfinished"></translation>
+        <translation>Hasło nie może być puste</translation>
     </message>
     <message>
         <location line="-8"/>
         <source>Enter passphrase to encrypt your session keys:</source>
-        <translation type="unfinished"></translation>
+        <translation>Wpisz frazÄ™ do odszyfrowania twoich kluczy sesji:</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>File to save the exported session keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Plik do którego zostaną wyeksportowane klucze sesji</translation>
+    </message>
+</context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Nie znaleziono zaszyfrowanego prywatnego czatu z tym użytkownikiem. Utwórz nowy zaszyfrowany prywatny czat z tym użytkownikiem i spróbuj ponownie.</translation>
     </message>
 </context>
 <context>
@@ -2468,22 +3031,22 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../qml/device-verification/Waiting.qml" line="+12"/>
         <source>Waiting for other party…</source>
-        <translation type="unfinished"></translation>
+        <translation>Oczekiwanie na rozmówcę...</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Waiting for other side to accept the verification request.</source>
-        <translation type="unfinished"></translation>
+        <translation>Oczekiwanie za zaakceptowanie prośby o weryfikację przez rozmówcę.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to continue the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Oczekiwanie na kontynuowanie weryfikacji przez rozmówcę.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Waiting for other side to complete the verification process.</source>
-        <translation type="unfinished"></translation>
+        <translation>Oczekiwanie na zakończenie procesu weryfikacji przez rozmówcę.</translation>
     </message>
     <message>
         <location line="+15"/>
@@ -2519,7 +3082,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/Utils.cpp" line="+184"/>
         <source>Yesterday</source>
-        <translation type="unfinished"></translation>
+        <translation>Wczoraj</translation>
     </message>
 </context>
 <context>
@@ -2527,7 +3090,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/CreateRoom.cpp" line="+40"/>
         <source>Create room</source>
-        <translation type="unfinished"></translation>
+        <translation>Utwórz pokój</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2570,7 +3133,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location filename="../../src/dialogs/FallbackAuth.cpp" line="+34"/>
         <source>Open Fallback in Browser</source>
-        <translation type="unfinished"></translation>
+        <translation>W razie konieczności otwórz w przeglądarce</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2580,43 +3143,12 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+1"/>
         <source>Confirm</source>
-        <translation type="unfinished"></translation>
+        <translation>Potwierdź</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Open the fallback, follow the steps and confirm after completing them.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Anuluj</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>ID pokoju lub alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Anuluj</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Czy na pewno chcesz wyjść?</translation>
+        <translation>Otwórz w przeglądarce i postępuj zgodnie z instrukcją.</translation>
     </message>
 </context>
 <context>
@@ -2664,7 +3196,7 @@ Rozmiar multimediów: %2
     <message>
         <location line="+1"/>
         <source>Confirm</source>
-        <translation type="unfinished"></translation>
+        <translation>Potwierdź</translation>
     </message>
     <message>
         <location line="+11"/>
@@ -2672,151 +3204,125 @@ Rozmiar multimediów: %2
         <translation>Rozwiąż reCAPTCHA i naciśnij przycisk „potwierdź”</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Potwierdzenia przeczytania</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished">Zamknij</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
         <location filename="../../src/Utils.h" line="+115"/>
         <source>You sent an audio clip</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłałeś(aś) klip audio</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent an audio clip</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) klip audio</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłałeś(aś) obraz</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) obraz</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłałeś(aś) plik</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) plik</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłałeś(aś) wideo</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) wideo</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłałeś(aś) naklejkę</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) naklejkę</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłałeś(aś) powiadomienie</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent a notification</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) powiadomienie</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>You: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Ty: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1: %2</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>You sent an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>Wysłałeś(aś) zaszyfrowaną wiadomość</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 sent an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wysłał(a) zaszyfrowaną wiadomość</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>You placed a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Wykonujesz telefon</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 wykonuje telefon</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Odebrałeś(aś) połączenie</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 odebrał(a) połączenie</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Zakończyłeś(aś) połączenie</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 zakończył(a) połączenie</translation>
     </message>
 </context>
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
-        <translation type="unfinished"></translation>
+        <translation>Nieznany Typ Wiadomości</translation>
     </message>
 </context>
 </TS>
diff --git a/resources/langs/nheko_pt_BR.ts b/resources/langs/nheko_pt_BR.ts
index b83585fbabf2848632ad22a7b89daffd59c8ab18..a98517095f58dc396e0a5878ed79629691bf2966 100644
--- a/resources/langs/nheko_pt_BR.ts
+++ b/resources/langs/nheko_pt_BR.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Ligando...</translation>
     </message>
@@ -40,7 +40,7 @@
     <message>
         <location filename="../qml/device-verification/AwaitingVerificationConfirmation.qml" line="+12"/>
         <source>Awaiting Confirmation</source>
-        <translation type="unfinished"></translation>
+        <translation>Aguardando Confirmação</translation>
     </message>
     <message>
         <location line="+12"/>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Chamada de Vídeo</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Chamada de Vídeo</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Tela Inteira</translation>
     </message>
@@ -125,49 +125,49 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Falha ao convidar usuário: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Usuário convidado: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao migrar cache para versão atual. Isso pode ter diferentes razões. Por favor reporte o problema e tente usar uma versão antiga no meio tempo. Alternativamente, você pode tentar excluir o cache manualmente.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Confirmar entrada</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to join %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Deseja realmente entrar em %1?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Sala %1 criada.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Confirmar convite</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Deseja realmente convidar %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Falha ao convidar %1 para %2: %3</translation>
     </message>
@@ -179,10 +179,10 @@
     <message>
         <location line="+1"/>
         <source>Do you really want to kick %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Deseja realmente expulsar %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Usuário expulso: %1</translation>
     </message>
@@ -194,10 +194,10 @@
     <message>
         <location line="+1"/>
         <source>Do you really want to ban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Deseja realmente banir %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Falha ao banir %1 em %2: %3</translation>
     </message>
@@ -214,10 +214,10 @@
     <message>
         <location line="+1"/>
         <source>Do you really want to unban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Deseja realmente desbanir %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Falha ao desbanir %1 em %2: %3</translation>
     </message>
@@ -227,75 +227,77 @@
         <translation>Usuário desbanido: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Deseja realmente iniciar uma conversa privada com %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Migração do cache falhou!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Incompatible cache version</source>
-        <translation type="unfinished"></translation>
+        <translation>Versão de cache incompatível</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache.</source>
-        <translation type="unfinished"></translation>
+        <translation>O cache em seu disco é mais recente do que essa versão do Nheko suporta. Por favor atualize ou limpe o cache.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Falha ao restaurar conta OLM. Por favor faça login novamente.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Falha ao restaurar dados salvos. Por favor faça login novamente.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao configurar chaves de criptografia. Resposta do servidor: %1 %2. Por favor tente novamente mais tarde.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao entrar na sala: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
-        <translation type="unfinished"></translation>
+        <translation>Você entrou na sala</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Failed to remove invite: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao remover o convite: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+18"/>
         <source>Failed to leave room: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao sair da sala: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao expulsar %1 de %2: %3</translation>
     </message>
 </context>
 <context>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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 filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">Cancelar</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished"></translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">Cancelar</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -756,25 +881,25 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -784,35 +909,53 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -862,18 +1005,23 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -901,7 +1049,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -924,7 +1072,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1073,32 +1233,28 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Nenhum microfone encontrado.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,28 +1391,18 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
+        <location line="+169"/>
+        <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
+        <location line="+5"/>
+        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
-        <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+26"/>
-        <source>The required endpoints were not found. Possibly not a Matrix server.</source>
+        <location line="+24"/>
+        <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1282,10 +1444,28 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1293,7 +1473,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1302,16 +1482,6 @@ Example: https://server.my:8787</source>
         <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1414,16 +1607,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1453,7 +1666,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1468,7 +1686,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1514,12 +1742,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1539,8 +1767,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1548,21 +1776,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1617,6 +1873,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Cancelar</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1669,18 +2040,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1700,7 +2071,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation type="unfinished">
@@ -1709,7 +2080,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1719,7 +2090,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1739,12 +2120,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1754,12 +2135,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1769,12 +2150,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1804,32 +2190,32 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished">Você entrou nessa sala.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1848,7 +2234,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1856,12 +2242,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1886,28 +2277,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1945,10 +2348,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1958,33 +2386,98 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+29"/>
+        <source>Room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2007,8 +2500,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2016,7 +2509,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2026,22 +2519,22 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2066,7 +2559,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2081,6 +2574,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2109,7 +2612,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2175,7 +2678,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Cancelar</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished">Cancelar</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2666,32 +3186,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3199,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2760,7 +3254,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2780,27 +3274,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2808,7 +3302,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_pt_PT.ts b/resources/langs/nheko_pt_PT.ts
index a0a8c8a83b715b72b50ebf7cdb2041dcc7208846..69eab896e94eee267e968f4d30b36681c31a8fb9 100644
--- a/resources/langs/nheko_pt_PT.ts
+++ b/resources/langs/nheko_pt_PT.ts
@@ -4,25 +4,25 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
-        <translation type="unfinished"></translation>
+        <translation>A chamar...</translation>
     </message>
     <message>
         <location line="+10"/>
         <location line="+10"/>
         <source>Connecting...</source>
-        <translation type="unfinished"></translation>
+        <translation>A ligar...</translation>
     </message>
     <message>
         <location line="+67"/>
         <source>You are screen sharing</source>
-        <translation type="unfinished"></translation>
+        <translation>Está a partilhar o seu ecrã</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Hide/Show Picture-in-Picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Mostrar/Ocultar Picture-in-Picture</translation>
     </message>
     <message>
         <location line="+13"/>
@@ -32,7 +32,7 @@
     <message>
         <location line="+0"/>
         <source>Mute Mic</source>
-        <translation type="unfinished"></translation>
+        <translation>Silenciar microfone</translation>
     </message>
 </context>
 <context>
@@ -40,262 +40,264 @@
     <message>
         <location filename="../qml/device-verification/AwaitingVerificationConfirmation.qml" line="+12"/>
         <source>Awaiting Confirmation</source>
-        <translation type="unfinished"></translation>
+        <translation>A aguardar confirmação</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Waiting for other side to complete verification.</source>
-        <translation type="unfinished"></translation>
+        <translation>A aguardar que o outro lado complete a verificação.</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation>Cancelar</translation>
     </message>
 </context>
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Videochamada</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Voice Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Chamada</translation>
     </message>
     <message>
         <location line="+62"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nenhum microfone encontrado.</translation>
     </message>
 </context>
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Videochamada</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Voice Call</source>
-        <translation type="unfinished"></translation>
+        <translation>Chamada</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Devices</source>
-        <translation type="unfinished"></translation>
+        <translation>Dispositivos</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Accept</source>
-        <translation type="unfinished"></translation>
+        <translation>Aceitar</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Unknown microphone: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Microfone desconhecido: %1</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Unknown camera: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Câmara desconhecida: %1</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Decline</source>
-        <translation type="unfinished"></translation>
+        <translation>Recusar</translation>
     </message>
     <message>
         <location line="-28"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nenhum microfone encontrado.</translation>
     </message>
 </context>
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Ecrã inteiro</translation>
     </message>
 </context>
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao convidar utilizador: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Utilizador convidado: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
-        <translation type="unfinished"></translation>
+        <translation>A migração da cache para a versão atual falhou, e existem várias razões possíveis. Por favor abra um problema (&quot;issue&quot;) e experimente usar uma versão mais antiga entretanto. Alternativamente, pode tentar apagar a cache manualmente.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
-        <translation type="unfinished"></translation>
+        <translation>Confirmar entrada</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to join %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Tem a certeza que quer entrar em %1?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
-        <translation type="unfinished"></translation>
+        <translation>Sala %1 criada.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Confirmar convite</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Tem a certeza que quer convidar %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao convidar %1 para %2: %3</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Confirm kick</source>
-        <translation type="unfinished"></translation>
+        <translation>Confirmar expulsão</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to kick %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Tem a certeza que quer expulsar %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Utilizador expulso: %1</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Confirm ban</source>
-        <translation type="unfinished"></translation>
+        <translation>Confirmar banimento</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to ban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Tem a certeza que quer banir %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao banir %1 em %2: %3</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Banned user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Utilizador banido: %1</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Confirm unban</source>
-        <translation type="unfinished"></translation>
+        <translation>Confirmar perdão</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Do you really want to unban %1 (%2)?</source>
-        <translation type="unfinished"></translation>
+        <translation>Tem a certeza que quer perdoar %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao perdoar %1 em %2: %3</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Unbanned user: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Utilizador perdoado: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Tem a certeza que quer começar uma conversa privada com %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao migrar a cache!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Incompatible cache version</source>
-        <translation type="unfinished"></translation>
+        <translation>Versão da cache incompatível</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache.</source>
-        <translation type="unfinished"></translation>
+        <translation>A cache que existe no seu disco é mais recente do que esta versão do Nheko suporta. Por favor atualize-a ou apague-a.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao restaurar a sua conta OLM. Por favor autentique-se novamente.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao restaurar dados guardados. Por favor, autentique-se novamente.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao estabelecer chaves encriptadas. Resposta do servidor: %1 %2. Tente novamente mais tarde.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Por favor, tente autenticar-se novamente: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao entrar em sala: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
-        <translation type="unfinished"></translation>
+        <translation>Entrou na sala</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Failed to remove invite: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao remover convite: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao criar sala: %1</translation>
     </message>
     <message>
         <location line="+18"/>
         <source>Failed to leave room: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao sair da sala: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao expulsar %1 de %2: %3</translation>
     </message>
 </context>
 <context>
@@ -303,7 +305,7 @@
     <message>
         <location filename="../qml/CommunitiesList.qml" line="+44"/>
         <source>Hide rooms with this tag or from this space by default.</source>
-        <translation type="unfinished"></translation>
+        <translation>Ocultar, por defeito, salas com esta etiqueta ou pertencentes a este espaço.</translation>
     </message>
 </context>
 <context>
@@ -311,70 +313,70 @@
     <message>
         <location filename="../../src/timeline/CommunitiesModel.cpp" line="+37"/>
         <source>All rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Todas as salas</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Shows all rooms without filtering.</source>
-        <translation type="unfinished"></translation>
+        <translation>Mostra todas as salas sem filtros.</translation>
     </message>
     <message>
         <location line="+30"/>
         <source>Favourites</source>
-        <translation type="unfinished"></translation>
+        <translation>Favoritos</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms you have favourited.</source>
-        <translation type="unfinished"></translation>
+        <translation>Salas favoritas.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Low Priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Prioridade baixa</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Rooms with low priority.</source>
-        <translation type="unfinished"></translation>
+        <translation>Salas com prioridade baixa.</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Server Notices</source>
-        <translation type="unfinished"></translation>
+        <translation>Avisos do servidor</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Messages from your server or administrator.</source>
-        <translation type="unfinished"></translation>
+        <translation>Mensagens do seu servidor ou administrador.</translation>
     </message>
 </context>
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
-        <translation type="unfinished"></translation>
+        <translation>Desencriptar segredos</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Enter your recovery key or passphrase to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Insira a sua chave de recuperação ou palavra-passe para desencriptar os seus segredos:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
-        <translation type="unfinished"></translation>
+        <translation>Insira a sua chave de recuperação ou palavra-passe chamada %1 para desencriptar os seus segredos:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao desencriptar</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Failed to decrypt secrets with the provided recovery key or passphrase</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao desencriptada segredos com a chave ou palavra-passe dada</translation>
     </message>
 </context>
 <context>
@@ -382,22 +384,22 @@
     <message>
         <location filename="../qml/device-verification/DigitVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>Código de verificação</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Please verify the following digits. You should see the same numbers on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
-        <translation type="unfinished"></translation>
+        <translation>Por favor verifique os seguintes dígitos.  Deve ver os mesmos em ambos os lados. Se forem diferentes, carregue em &quot;Não coincidem!&quot; para abortar a verificação!</translation>
     </message>
     <message>
         <location line="+31"/>
         <source>They do not match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Não coincidem!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>They match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Coincidem!</translation>
     </message>
 </context>
 <context>
@@ -405,22 +407,22 @@
     <message>
         <location filename="../../src/ui/RoomSettings.cpp" line="+42"/>
         <source>Apply</source>
-        <translation type="unfinished"></translation>
+        <translation>Aplicar</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation>Cancelar</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Name</source>
-        <translation type="unfinished"></translation>
+        <translation>Nome</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Topic</source>
-        <translation type="unfinished"></translation>
+        <translation>Tópico</translation>
     </message>
 </context>
 <context>
@@ -428,47 +430,47 @@
     <message>
         <location filename="../qml/emoji/EmojiPicker.qml" line="+68"/>
         <source>Search</source>
-        <translation type="unfinished"></translation>
+        <translation>Procurar</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
-        <translation type="unfinished"></translation>
+        <translation>Pessoas</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Nature</source>
-        <translation type="unfinished"></translation>
+        <translation>Natureza</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Food</source>
-        <translation type="unfinished"></translation>
+        <translation>Comida</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Activity</source>
-        <translation type="unfinished"></translation>
+        <translation>Actividades</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Travel</source>
-        <translation type="unfinished"></translation>
+        <translation>Viagem</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Objects</source>
-        <translation type="unfinished"></translation>
+        <translation>Objetos</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Symbols</source>
-        <translation type="unfinished"></translation>
+        <translation>Símbolos</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Flags</source>
-        <translation type="unfinished"></translation>
+        <translation>Bandeiras</translation>
     </message>
 </context>
 <context>
@@ -476,90 +478,88 @@
     <message>
         <location filename="../qml/device-verification/EmojiVerification.qml" line="+11"/>
         <source>Verification Code</source>
-        <translation type="unfinished"></translation>
+        <translation>Código de verificação</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press &apos;They do not match!&apos; to abort verification!</source>
-        <translation type="unfinished"></translation>
+        <translation>Por favor verifique os seguintes emojis. Deve ver os mesmos em ambos os lados. Se não coincidirem, carregue em &quot;Não coincidem!&quot; para abortar a verificação!</translation>
     </message>
     <message>
         <location line="+376"/>
         <source>They do not match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Não coincidem!</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>They match!</source>
-        <translation type="unfinished"></translation>
+        <translation>Coincidem!</translation>
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation type="unfinished"></translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation>Não existe nenhuma chave para desbloquear esta mensagem. Nós pedimos a chave automaticamente, mas pode tentar pedi-la outra vez se estiver impaciente.</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation>Esta mensagem não pôde ser desencriptada, porque apenas temos uma chave para mensagens mais recentes. Pode tentar solicitar acesso a esta mensagem.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation type="unfinished"></translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation>Ocorreu um erro interno ao ler a chave de desencriptação da base de dados.</translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation type="unfinished"></translation>
+        <source>There was an error decrypting this message.</source>
+        <translation>Ocorreu um erro ao desencriptar esta mensagem.</translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation>Esta mensagem não pôde ser processada.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation>Esta chave de encriptação foi reutilizada! É possível que alguém esteja a tentar inserir mensagens falsas nesta conversa!</translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation>Erro de desencriptação desconhecido</translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation type="unfinished"></translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation>Solicitar chave</translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Esta mensagem não está encriptada!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Encriptado por um dispositivo verificado.</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation type="unfinished"></translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Encriptado por um dispositivo não verificado, mas até agora tem confiado neste utilizador.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation>Encriptado por um dispositivo não verificado ou a chave é de uma fonte não confiável, como o backup da chave.</translation>
     </message>
 </context>
 <context>
@@ -567,315 +567,467 @@
     <message>
         <location filename="../qml/device-verification/Failed.qml" line="+11"/>
         <source>Verification failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao verifcar</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>Other client does not support our verification protocol.</source>
-        <translation type="unfinished"></translation>
+        <translation>O outro cliente não suporta o nosso protocolo de verificação.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Key mismatch detected!</source>
-        <translation type="unfinished"></translation>
+        <translation>Detetada divergência de chaves!</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
-        <translation type="unfinished"></translation>
+        <translation>A verificação do dispositivo expirou.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
+        <translation>A outra parte cancelou a verificação.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
-        <source>Close</source>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+14"/>
+        <source>Close</source>
+        <translation>Fechar</translation>
+    </message>
 </context>
 <context>
     <name>ForwardCompleter</name>
     <message>
         <location filename="../qml/ForwardCompleter.qml" line="+44"/>
         <source>Forward Message</source>
+        <translation>Reencaminhar mensagem</translation>
+    </message>
+</context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation>A editar pacote de imagens</translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation>Adicionar imagens</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation>Autocolantes (*.png *.webp *.gif *.jpg *.jpeg)</translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation>Nome do pacote</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation>Usar como emoji</translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation>Usar como autocolante</translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished">Código</translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished">Corpo</translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation>Remover do pacote</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation>Remover</translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation>Cancelar</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation>Guardar</translation>
+    </message>
 </context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
         <location filename="../qml/dialogs/ImagePackSettingsDialog.qml" line="+22"/>
         <source>Image pack settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Definições do pacote de imagens</translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation>Criar pacote de conta</translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation>Criar pacote de sala</translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Pacote privado</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Pack from this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Pacote desta sala</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Globally enabled pack</source>
-        <translation type="unfinished"></translation>
+        <translation>Pacote ativo globalmente</translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
-        <translation type="unfinished"></translation>
+        <translation>Ativar globalmente</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Enables this pack to be used in all rooms</source>
-        <translation type="unfinished"></translation>
+        <translation>Permite que o pacote seja usado em todas as salas</translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation>Editar</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
-        <translation type="unfinished"></translation>
+        <translation>Fechar</translation>
     </message>
 </context>
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
-        <translation type="unfinished"></translation>
+        <translation>Selecionar um ficheiro</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation>Todos os ficheiros (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao carregar mídia. Por favor, tente novamente.</translation>
     </message>
 </context>
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Convidar utilizadores para %1</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>User ID to invite</source>
-        <translation type="unfinished"></translation>
+        <translation>ID do utilizador a convidar</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>@joe:matrix.org</source>
         <comment>Example user id. The name &apos;joe&apos; can be localized however you want.</comment>
-        <translation type="unfinished"></translation>
+        <translation>@ze:matrix.org</translation>
     </message>
     <message>
         <location line="+17"/>
         <source>Add</source>
-        <translation type="unfinished"></translation>
+        <translation>Adicionar</translation>
     </message>
     <message>
         <location line="+58"/>
         <source>Invite</source>
-        <translation type="unfinished"></translation>
+        <translation>Convidar</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation>Cancelar</translation>
     </message>
 </context>
 <context>
-    <name>LoginPage</name>
+    <name>JoinRoomDialog</name>
     <message>
-        <location filename="../../src/LoginPage.cpp" line="+81"/>
-        <source>Matrix ID</source>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
-        <source>e.g @joe:matrix.org</source>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
     <message>
-        <location line="+2"/>
-        <source>Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :.
-You can also put your homeserver address there, if your server doesn&apos;t support .well-known lookup.
-Example: @user:server.my
-If Nheko fails to discover your homeserver, it will show you a field to enter the server manually.</source>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Sair da sala</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>LoginPage</name>
+    <message>
+        <location filename="../../src/LoginPage.cpp" line="+81"/>
+        <source>Matrix ID</source>
+        <translation>ID Matrix</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>e.g @joe:matrix.org</source>
+        <translation>p. ex. @ze:matrix.org</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :.
+You can also put your homeserver address there, if your server doesn&apos;t support .well-known lookup.
+Example: @user:server.my
+If Nheko fails to discover your homeserver, it will show you a field to enter the server manually.</source>
+        <translation>O seu nome de utilizador. 
+Um ID Matrix (MXID) deve iniciar com um @ seguido pelo ID do utilizador, uns :, e por fim, o nome do seu servidor. Pode, também, colocar o seu endereço, caso este não suporte pesquisas &quot;.well-known&quot;.
+Exemplo: @utilizador:servidor.meu
+Se o Nheko não conseguir encontrar o seu servidor, irá apresentar um campo onde deve inserir o endereço manualmente.</translation>
+    </message>
     <message>
         <location line="+25"/>
         <source>Password</source>
-        <translation type="unfinished"></translation>
+        <translation>Palavra-passe</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your password.</source>
-        <translation type="unfinished"></translation>
+        <translation>A sua palavra-passe</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Device name</source>
-        <translation type="unfinished"></translation>
+        <translation>Nome do dispositivo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.</source>
-        <translation type="unfinished"></translation>
+        <translation>Um nome para este dispositivo, que será exibido noutros quando os estiver a verificar. Caso nenhum seja fornecido, será usado um pré-definido.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Homeserver address</source>
-        <translation type="unfinished"></translation>
+        <translation>Endereço do servidor</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>servidor.meu:8787</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>The address that can be used to contact you homeservers client API.
 Example: https://server.my:8787</source>
-        <translation type="unfinished"></translation>
+        <translation>O endereço que pode ser usado para contactar a API de clientes do seu servidor.
+Exemplo: https://servidor.meu:8787</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation>INCIAR SESSÃO</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
-        <translation type="unfinished"></translation>
+        <translation>Inseriu um ID Matrix inválido  p. ex. @ze:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha na descoberta automática. Reposta mal formatada recebida.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha na descoberta automática. Erro desconhecido ao solicitar &quot;.well-known&quot;.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished"></translation>
+        <translation>Não foi possível encontrar os funções (&quot;endpoints&quot;) necessárias. Possivelmente não é um servidor Matrix.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Resposta mal formada recebida. Certifique-se que o domínio do servidor está correto.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Erro desconhecido. Certifique-se que o domínio do servidor é válido.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">ENTRAR COM ISU (SSO)</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
-        <translation type="unfinished"></translation>
+        <translation>Palavra-passe vazia</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
+        <translation>Falha no ISU (SSO)</translation>
+    </message>
+</context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
-        <translation type="unfinished"></translation>
+        <translation>Encriptação ativada</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>nome da sala alterado para: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed room name</source>
-        <translation type="unfinished"></translation>
+        <translation>nome da sala removido</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>topic changed to: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>tópico da sala alterado para: %1</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>removed topic</source>
-        <translation type="unfinished"></translation>
+        <translation>tópico da sala removido</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 changed the room avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 alterou o ícone da sala</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>%1 created and configured room: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 criou e configurou a sala: %2</translation>
     </message>
     <message>
         <location line="+15"/>
         <source>%1 placed a voice call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 iniciou uma chamada de voz.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 placed a video call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 iniciou uma chamada de vídeo.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 placed a call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 iniciou uma chamada.</translation>
     </message>
     <message>
         <location line="+38"/>
         <source>Negotiating call...</source>
-        <translation type="unfinished"></translation>
+        <translation>A negociar chamada…</translation>
+    </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation>Permitir a entrada</translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 atendeu a chamada.</translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
-        <translation type="unfinished"></translation>
+        <translation>removida</translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 terminou a chamada.</translation>
     </message>
 </context>
 <context>
@@ -883,135 +1035,142 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/MessageInput.qml" line="+44"/>
         <source>Hang up</source>
-        <translation type="unfinished"></translation>
+        <translation>Desligar</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Place a call</source>
-        <translation type="unfinished"></translation>
+        <translation>Iniciar chamada</translation>
     </message>
     <message>
         <location line="+25"/>
         <source>Send a file</source>
-        <translation type="unfinished"></translation>
+        <translation>Enviar um ficheiro</translation>
     </message>
     <message>
         <location line="+50"/>
         <source>Write a message...</source>
-        <translation type="unfinished"></translation>
+        <translation>Escreva uma mensagem…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
-        <translation type="unfinished"></translation>
+        <translation>Autocolantes</translation>
     </message>
     <message>
         <location line="+24"/>
         <source>Emoji</source>
-        <translation type="unfinished"></translation>
+        <translation>Emoji</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Send</source>
-        <translation type="unfinished"></translation>
+        <translation>Enviar</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>You don&apos;t have permission to send messages in this room</source>
-        <translation type="unfinished"></translation>
+        <translation>Não tem permissão para enviar mensagens nesta sala</translation>
     </message>
 </context>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>Editar</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>React</source>
-        <translation type="unfinished"></translation>
+        <translation>Reagir</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Reply</source>
-        <translation type="unfinished"></translation>
+        <translation>Responder</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Options</source>
-        <translation type="unfinished"></translation>
+        <translation>Opções</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Copiar</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
-        <translation type="unfinished"></translation>
+        <translation>Copiar localização da &amp;ligação</translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
-        <translation type="unfinished"></translation>
+        <translation>Re&amp;agir</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Repl&amp;y</source>
-        <translation type="unfinished"></translation>
+        <translation>Responde&amp;r</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Edit</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Editar</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Read receip&amp;ts</source>
-        <translation type="unfinished"></translation>
+        <translation>Recibos de &amp;leitura</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>&amp;Forward</source>
-        <translation type="unfinished"></translation>
+        <translation>Reen&amp;caminhar</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>&amp;Mark as read</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Marcar como lida</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>View raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Ver mensagem bruta</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>View decrypted raw message</source>
-        <translation type="unfinished"></translation>
+        <translation>Ver mensagem bruta desencriptada</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Remo&amp;ve message</source>
-        <translation type="unfinished"></translation>
+        <translation>Remo&amp;ver mensagem</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Save as</source>
-        <translation type="unfinished"></translation>
+        <translation>&amp;Guardar como</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>&amp;Open in external program</source>
-        <translation type="unfinished"></translation>
+        <translation>Abrir num &amp;programa externo</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Copy link to eve&amp;nt</source>
-        <translation type="unfinished"></translation>
+        <translation>Copiar ligação para o eve&amp;nto</translation>
+    </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation>Ir para mensagem &amp;citada</translation>
     </message>
 </context>
 <context>
@@ -1019,56 +1178,69 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/NewVerificationRequest.qml" line="+11"/>
         <source>Send Verification Request</source>
-        <translation type="unfinished"></translation>
+        <translation>Enviar pedido de verificação</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Received Verification Request</source>
+        <translation>Pedido de verificação recebido</translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
-        <translation type="unfinished"></translation>
+        <translation>Para que outros possam ver que dispositivos pertencem realmente a si, pode verificá-los. Isso permite, também, que a cópia de segurança de chaves funcione automaticamente. Verificar %1 agora?</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.</source>
-        <translation type="unfinished"></translation>
+        <translation>Para garantir que nenhum utilizador mal-intencionado possa intercetar as suas comunicações encriptadas, pode verificar a outra parte.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 has requested to verify their device %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 requisitou a verificação do seu dispositivo %2.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 using the device %2 has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1, usando o dispositivo %2, requisitou a sua verificação.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Your device (%1) has requested to be verified.</source>
-        <translation type="unfinished"></translation>
+        <translation>O seu dispositivo (%1) requisitou a sua verificação.</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation>Cancelar</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Deny</source>
-        <translation type="unfinished"></translation>
+        <translation>Recusar</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Start verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Iniciar verificação</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Accept</source>
+        <translation>Aceitar</translation>
+    </message>
+</context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1076,44 +1248,32 @@ Example: https://server.my:8787</source>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 enviou uma mensagem encriptada</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 respondeu: %2</translation>
     </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
         <source>%1 replied with an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 respondeu com uma mensagem encriptada</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>%1 replied to a message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 respondeu a uma mensagem</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>%1 sent a message</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 enviou uma mensagem</translation>
     </message>
 </context>
 <context>
@@ -1121,32 +1281,32 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/PlaceCall.qml" line="+48"/>
         <source>Place a call to %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Iniciar chamada para %1?</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>No microphone found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nenhum microfone encontrado.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
-        <translation type="unfinished"></translation>
+        <translation>Voz</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Video</source>
-        <translation type="unfinished"></translation>
+        <translation>Vídeo</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Ecrã</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation>Cancelar</translation>
     </message>
 </context>
 <context>
@@ -1154,412 +1314,512 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/delegates/Placeholder.qml" line="+11"/>
         <source>unimplemented event: </source>
-        <translation type="unfinished"></translation>
+        <translation>evento não implementado: </translation>
     </message>
 </context>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
-        <translation type="unfinished"></translation>
+        <translation>Crie um perfil único que lhe permite entrar em várias contas simultaneamente e iniciar várias instâncias do Nheko.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>profile</source>
-        <translation type="unfinished"></translation>
+        <translation>perfil</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>profile name</source>
-        <translation type="unfinished"></translation>
+        <translation>nome de perfil</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation>Recibos de leitura</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation>Ontem, %1</translation>
     </message>
 </context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
-        <translation type="unfinished"></translation>
+        <translation>Nome de utilizador</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
-        <translation type="unfinished"></translation>
+        <translation>O nome de utilizador não pode ser vazio e tem que conter apenas os caracteres a-z, 0-9, ., _, =, - e /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
-        <translation type="unfinished"></translation>
+        <translation>Palavra-passe</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Please choose a secure password. The exact requirements for password strength may depend on your server.</source>
-        <translation type="unfinished"></translation>
+        <translation>Por favor, escolha uma palavra-passe segura. Os requisitos exatos para a força da palavra-passe poderão depender no seu servidor.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Password confirmation</source>
-        <translation type="unfinished"></translation>
+        <translation>Confirmação da palavra-passe</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Homeserver</source>
-        <translation type="unfinished"></translation>
+        <translation>Servidor</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
-        <translation type="unfinished"></translation>
+        <translation>Um servidor que permita registos. Uma vez que a Matrix é descentralizada, o utilizador precisa primeiro de encontrar um servidor onde se possa registar, ou alojar o seu próprio.</translation>
     </message>
     <message>
         <location line="+35"/>
         <source>REGISTER</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>REGISTAR</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha na descoberta automática. Resposta mal formada recebida.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha na descoberta automática. Erro desconhecido ao requisitar &quot;.well-known&quot;.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
-        <translation type="unfinished"></translation>
+        <translation>Não foi possível encontrar os funções (&quot;endpoints&quot;) necessárias. Possivelmente não é um servidor Matrix.</translation>
     </message>
     <message>
         <location line="+6"/>
         <source>Received malformed response. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Resposta mal formada recebida. Certifique-se que o domínio do servidor está correto.</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
-        <translation type="unfinished"></translation>
+        <translation>Erro desconhecido. Certifique-se que o domínio do servidor é válido.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
-        <translation type="unfinished"></translation>
+        <translation>A palavra-passe não é longa o suficiente (mín, 8 caracteres)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
-        <translation type="unfinished"></translation>
+        <translation>As palavras-passe não coincidem</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
-        <translation type="unfinished"></translation>
+        <translation>Nome do servidor inválido</translation>
     </message>
 </context>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
-        <translation type="unfinished"></translation>
+        <translation>Fechar</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Cancel edit</source>
+        <translation>Cancelar edição</translation>
+    </message>
+</context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation>Explorar salas públicas</translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation>Procurar por salas públicas</translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
-        <translation type="unfinished"></translation>
+        <translation>nenhuma versão guardada</translation>
     </message>
 </context>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
-        <translation type="unfinished"></translation>
+        <translation>Nova etiqueta</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter the tag you want to use:</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
+        <translation>Insira a etiqueta que quer usar:</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
-        <translation type="unfinished"></translation>
+        <translation>Sair da sala</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Tag room as:</source>
-        <translation type="unfinished"></translation>
+        <translation>Etiquetar sala com:</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>Favourite</source>
-        <translation type="unfinished"></translation>
+        <translation>Favoritos</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Low priority</source>
-        <translation type="unfinished"></translation>
+        <translation>Prioridade baixa</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Server notice</source>
-        <translation type="unfinished"></translation>
+        <translation>Avisos do servidor</translation>
     </message>
     <message>
         <location line="+13"/>
         <source>Create new tag...</source>
-        <translation type="unfinished"></translation>
+        <translation>Criar nova etiqueta...</translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
-        <translation type="unfinished"></translation>
+        <translation>Mensagem de estado</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Enter your status message:</source>
-        <translation type="unfinished"></translation>
+        <translation>Insira a sua mensagem de estado:</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Profile settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Definições de perfil</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Set status message</source>
-        <translation type="unfinished"></translation>
+        <translation>Definir mensagem de estado</translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
+        <translation>Terminar sessão</translation>
+    </message>
+    <message>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
-        <source>Start a new chat</source>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Fechar</translation>
+    </message>
+    <message>
+        <location line="+65"/>
+        <source>Start a new chat</source>
+        <translation>Iniciar uma nova conversa</translation>
+    </message>
     <message>
         <location line="+8"/>
         <source>Join a room</source>
-        <translation type="unfinished"></translation>
+        <translation>Entrar numa sala</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Create a new room</source>
-        <translation type="unfinished"></translation>
+        <translation>Criar uma nova sala</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Room directory</source>
-        <translation type="unfinished"></translation>
+        <translation>Diretório de salas</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Definições de utilizador</translation>
     </message>
 </context>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Membros de %1</translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%n pessoa em %1</numerusform>
+            <numerusform>%n pessoas em %1</numerusform>
         </translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Invite more people</source>
-        <translation type="unfinished"></translation>
+        <translation>Convidar mais pessoas</translation>
+    </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation>Esta sala não está encriptada!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation>Este utilizador está verificado.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation>Este utilizador não está verificado, mas continua a usar a mesma chave-mestra da primeira vez que se conheceram.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation>Este utilizador tem dispositivos não verificados!</translation>
     </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Definições de sala</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 membro(s)</translation>
     </message>
     <message>
         <location line="+55"/>
         <source>SETTINGS</source>
-        <translation type="unfinished"></translation>
+        <translation>DEFINIÇŎES</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Notifications</source>
-        <translation type="unfinished"></translation>
+        <translation>Notificações</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Muted</source>
-        <translation type="unfinished"></translation>
+        <translation>Silenciada</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Mentions only</source>
-        <translation type="unfinished"></translation>
+        <translation>Apenas menções</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All messages</source>
-        <translation type="unfinished"></translation>
+        <translation>Todas as mensagens</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation>Acesso à sala</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
-        <translation type="unfinished"></translation>
+        <translation>Qualquer pessoa e visitantes</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Anyone</source>
-        <translation type="unfinished"></translation>
+        <translation>Qualquer pessoa</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Invited users</source>
-        <translation type="unfinished"></translation>
+        <translation>Utilizadores convidados</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation>&quot;Batendo à porta&quot;</translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation>Impedido por participação noutras salas</translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
-        <translation type="unfinished"></translation>
+        <translation>Encriptação</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>End-to-End Encryption</source>
-        <translation type="unfinished"></translation>
+        <translation>Encriptação ponta-a-ponta</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
                             Please take note that it can&apos;t be disabled afterwards.</source>
-        <translation type="unfinished"></translation>
+        <translation>A encriptação é, de momento, experimental e certas coisas podem partir-se inesperadamente.&lt;br&gt;Por favor, tome nota de que depois não pode ser desativada.</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>Sticker &amp; Emote Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Definições de autocolantes e emojis</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>Change</source>
-        <translation type="unfinished"></translation>
+        <translation>Alterar</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Change what packs are enabled, remove packs or create new ones</source>
-        <translation type="unfinished"></translation>
+        <translation>Alterar a seleção de pacotes ativos, remover e criar novos pacotes</translation>
     </message>
     <message>
         <location line="+16"/>
         <source>INFO</source>
-        <translation type="unfinished"></translation>
+        <translation>INFO</translation>
     </message>
     <message>
         <location line="+9"/>
         <source>Internal ID</source>
-        <translation type="unfinished"></translation>
+        <translation>ID interno</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Room Version</source>
-        <translation type="unfinished"></translation>
+        <translation>Versão da sala</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao ativar encriptação: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
-        <translation type="unfinished"></translation>
+        <translation>Selecionar um ícone</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation>Todos os ficheiros (*)</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>The selected file is not an image</source>
-        <translation type="unfinished"></translation>
+        <translation>O ficheiro selecionado não é uma imagem</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Error while reading file: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Erro ao ler ficheiro: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao carregar imagem: %s</translation>
     </message>
 </context>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>Convite pendente.</translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
-        <translation type="unfinished"></translation>
+        <translation>A pré-visualizar esta sala</translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
+        <translation>Pré-visualização não disponível</translation>
+    </message>
+</context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -1568,27 +1828,27 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/voip/ScreenShare.qml" line="+30"/>
         <source>Share desktop with %1?</source>
-        <translation type="unfinished"></translation>
+        <translation>Partilhar ambiente de trabalho com %1?</translation>
     </message>
     <message>
         <location line="+11"/>
         <source>Window:</source>
-        <translation type="unfinished"></translation>
+        <translation>Janela:</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Frame rate:</source>
-        <translation type="unfinished"></translation>
+        <translation>Taxa de fotogramas:</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Include your camera picture-in-picture</source>
-        <translation type="unfinished"></translation>
+        <translation>Incluir a sua câmara em miniatura</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Request remote camera</source>
-        <translation type="unfinished"></translation>
+        <translation>Requisitar câmara remota</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -1599,45 +1859,160 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+5"/>
         <source>Hide mouse cursor</source>
-        <translation type="unfinished"></translation>
+        <translation>Esconder cursor do rato</translation>
     </message>
     <message>
         <location line="+20"/>
         <source>Share</source>
-        <translation type="unfinished"></translation>
+        <translation>Partilhar</translation>
     </message>
     <message>
         <location line="+19"/>
         <source>Preview</source>
-        <translation type="unfinished"></translation>
+        <translation>Pré-visualizar</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>Cancel</source>
+        <translation>Cancelar</translation>
+    </message>
+</context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation>Falha ao ligar ao armazenamento secreto</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation>Falha ao atualizar pacote de imagem: %1</translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation>Falha ao eliminar pacote de imagem antigo: %1</translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation>Falha ao abrir imagem: %1</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation>Falha ao carregar imagem: %1</translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
         <location filename="../qml/StatusIndicator.qml" line="+24"/>
         <source>Failed</source>
-        <translation type="unfinished"></translation>
+        <translation>Falhou</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Sent</source>
-        <translation type="unfinished"></translation>
+        <translation>Enviado</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Received</source>
-        <translation type="unfinished"></translation>
+        <translation>Recebido</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Read</source>
-        <translation type="unfinished"></translation>
+        <translation>Lido</translation>
     </message>
 </context>
 <context>
@@ -1645,7 +2020,7 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/emoji/StickerPicker.qml" line="+70"/>
         <source>Search</source>
-        <translation type="unfinished"></translation>
+        <translation>Procurar</translation>
     </message>
 </context>
 <context>
@@ -1653,283 +2028,315 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../qml/device-verification/Success.qml" line="+11"/>
         <source>Successful Verification</source>
-        <translation type="unfinished"></translation>
+        <translation>Verificação bem sucedida</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Verification successful! Both sides verified their devices!</source>
-        <translation type="unfinished"></translation>
+        <translation>Verificação bem sucedida!  Ambos os lados verificaram os seus dispositivos!</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>Close</source>
-        <translation type="unfinished"></translation>
+        <translation>Fechar</translation>
     </message>
 </context>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao eliminar mensagem: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
-        <translation type="unfinished"></translation>
+        <translation>Falha ao encriptar evento, envio abortado!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
-        <translation type="unfinished"></translation>
+        <translation>Guardar imagem</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save video</source>
-        <translation type="unfinished"></translation>
+        <translation>Guardar vídeo</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save audio</source>
-        <translation type="unfinished"></translation>
+        <translation>Guardar áudio</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Save file</source>
-        <translation type="unfinished"></translation>
+        <translation>Guardar ficheiro</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
-        <translation type="unfinished">
-            <numerusform></numerusform>
-            <numerusform></numerusform>
+        <translation>
+            <numerusform>%1%2 está a escrever...</numerusform>
+            <numerusform>%1 e %2 estão a escrever...</numerusform>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 abriu a sala ao público.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 made this room require and invitation to join.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 tornou esta sala acessível apenas por convite.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation>%1 tornou possível entrar na sala &quot;batendo à porta&quot;.</translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation>%1 autorizou os membros das seguintes salas a juntarem-se à sala automaticamente: %2</translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 tornou a sala aberta a visitantes.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 has closed the room to guest access.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 fechou o acesso à sala a visitantes.</translation>
     </message>
     <message>
         <location line="+23"/>
         <source>%1 made the room history world readable. Events may be now read by non-joined people.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 tornou o histórico da sala visível a qualquer pessoa. Eventos podem agora ser lidos por não-membros.</translation>
     </message>
     <message>
         <location line="+4"/>
         <source>%1 set the room history visible to members from this point on.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">%1 tornou o histórico da sala, a partir deste momento, visível a membros</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">%1 tornou o histórico visível a membros desde o seu convite.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">%1 tornou o histórico da sala visível ao membros desde a sua entrada.</translation>
     </message>
     <message>
         <location line="+22"/>
         <source>%1 has changed the room&apos;s permissions.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 alterou as permissões da sala.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 foi convidado.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 alterou o seu avatar.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 changed some profile info.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 alterou alguma informação de perfil.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 entrou.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation>%1 entrou com autorização do servidor de %2.</translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 recusou o seu convite.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Revoked the invite to %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Convite de %1 cancelado.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>%1 left the room.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 saiu da sala.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Kicked %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 foi expulso.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Unbanned %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 foi perdoado.</translation>
     </message>
     <message>
         <location line="+14"/>
         <source>%1 was banned.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 foi banido.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Razão: %1</translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 eliminou a sua &quot;batida à porta&quot;.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
-        <translation type="unfinished"></translation>
+        <translation>Entrou na sala.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 alterou o seu avatar e também o seu nome de exibição para %2.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 alterou o seu nome de exibição para %2.</translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Recusada a batida de %1.</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>%1 left after having already left!</source>
         <comment>This is a leave event after the user already left and shouldn&apos;t happen apart from state resets</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 saiu depois de já ter saído!</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>%1 knocked.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 bateu à porta.</translation>
     </message>
 </context>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
-        <translation type="unfinished"></translation>
+        <translation>Editada</translation>
     </message>
 </context>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
-        <translation type="unfinished"></translation>
+        <translation>Nenhuma sala aberta</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished">Pré-visualização não disponível</translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 membro(s)</translation>
     </message>
     <message>
         <location line="+33"/>
         <source>join the conversation</source>
-        <translation type="unfinished"></translation>
+        <translation>juntar-se à conversa</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>accept invite</source>
-        <translation type="unfinished"></translation>
+        <translation>aceitar convite</translation>
     </message>
     <message>
         <location line="+7"/>
         <source>decline invite</source>
-        <translation type="unfinished"></translation>
+        <translation>recusar convite</translation>
     </message>
     <message>
         <location line="+27"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
+        <translation>Voltar à lista de salas</translation>
     </message>
 </context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
-        <translation type="unfinished"></translation>
+        <translation>Voltar à lista de salas</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
+        <translation>Nenhuma sala selecionada</translation>
+    </message>
+    <message>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation>Esta sala não é encriptada!</translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation>Esta sala contém apenas dispositivos verificados.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation>Esta sala contém dispositivos não verificados!</translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
-        <translation type="unfinished"></translation>
+        <translation>Opções da sala</translation>
     </message>
     <message>
         <location line="+8"/>
         <source>Invite users</source>
-        <translation type="unfinished"></translation>
+        <translation>Convidar utilizadores</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Members</source>
-        <translation type="unfinished"></translation>
+        <translation>Membros</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Leave room</source>
-        <translation type="unfinished"></translation>
+        <translation>Sair da sala</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Definições</translation>
     </message>
 </context>
 <context>
@@ -1937,123 +2344,213 @@ Example: https://server.my:8787</source>
     <message>
         <location filename="../../src/TrayIcon.cpp" line="+112"/>
         <source>Show</source>
-        <translation type="unfinished"></translation>
+        <translation>Mostrar</translation>
     </message>
     <message>
         <location line="+1"/>
         <source>Quit</source>
+        <translation>Sair</translation>
+    </message>
+</context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished">Por favor, insira um testemunho de registo válido.</translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
-        <translation type="unfinished"></translation>
+        <translation>Perfil de utilizador global</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>Room User Profile</source>
-        <translation type="unfinished"></translation>
+        <translation>Perfil de utilizador na sala</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation>Alterar avatar globalmente.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation>Alterar avatar. Irá apenas afetar esta sala.</translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation>Alterar nome de exibição globalmente.</translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation>Alterar nome de exibição. Irá apenas afetar esta sala.</translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation>Sala: %1</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation>Este é um perfil específico desta sala. O nome e avatar do utilizador poderão ser diferentes dos seus homólogos globais.</translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation>Abrir o perfil global deste utilizador.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
+        <translation>Verificar</translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation>Iniciar uma conversa privada.</translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation>Expulsar o utilizador.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation>Banir o utilizador.</translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+31"/>
+        <source>Change device name.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+27"/>
         <source>Unverify</source>
+        <translation>Anular verificação</translation>
+    </message>
+    <message>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
-        <source>Select an avatar</source>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+223"/>
+        <source>Select an avatar</source>
+        <translation>Selecionar um avatar</translation>
+    </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation>Todos os ficheiros (*)</translation>
     </message>
     <message>
         <location line="+12"/>
         <source>The selected file is not an image</source>
-        <translation type="unfinished"></translation>
+        <translation>O ficheiro selecionado não é uma imagem</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>Error while reading file: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Erro ao ler ficheiro: %1</translation>
     </message>
 </context>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Padrão</translation>
     </message>
 </context>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Minimizar para bandeja</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Start in tray</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Iniciar na bandeja</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
-        <translation type="unfinished"></translation>
+        <translation>Barra lateral do grupo</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
-        <translation type="unfinished"></translation>
+        <translation>Avatares circulares</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>perfil: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Padrão</translation>
     </message>
     <message>
         <location line="+31"/>
         <source>CALLS</source>
-        <translation type="unfinished"></translation>
+        <translation>CHAMADAS</translation>
     </message>
     <message>
         <location line="+46"/>
         <source>Cross Signing Keys</source>
-        <translation type="unfinished"></translation>
+        <translation>Chaves de assinatura cruzada</translation>
     </message>
     <message>
         <location line="+4"/>
@@ -2063,12 +2560,12 @@ Example: https://server.my:8787</source>
     <message>
         <location line="+1"/>
         <source>DOWNLOAD</source>
-        <translation type="unfinished"></translation>
+        <translation>DESCARREGAR</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
-        <translation type="unfinished"></translation>
+        <translation>Manter a aplicação a correr em segundo plano depois de fechar a janela.</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -2081,6 +2578,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2100,25 +2607,28 @@ Only affects messages in encrypted chats.</source>
     <message>
         <location line="+2"/>
         <source>Privacy Screen</source>
-        <translation type="unfinished"></translation>
+        <translation>Ecrã de privacidade</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>When the window loses focus, the timeline will
 be blurred.</source>
-        <translation type="unfinished"></translation>
+        <translation>Quando a janela perde a atenção, a cronologia 
+será desfocada.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
-        <translation type="unfinished"></translation>
+        <translation>Tempo de inatividade para ecrã de privacidade (em segundos [0 - 3600])</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Set timeout (in seconds) for how long after window loses
 focus before the screen will be blurred.
 Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds)</source>
-        <translation type="unfinished"></translation>
+        <translation>Definir tempo (em segundos) depois da janela perder a 
+atenção até que o ecrã seja desfocado.
+Defina como 0 para desfocar imediatamente após perder a atenção. Valor máximo de 1 hora (3600 segundos)</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -2143,41 +2653,45 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds
     <message>
         <location line="+2"/>
         <source>Typing notifications</source>
-        <translation type="unfinished"></translation>
+        <translation>Notificações de escrita</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show who is typing in a room.
 This will also enable or disable sending typing notifications to others.</source>
-        <translation type="unfinished"></translation>
+        <translation>Mostrar quem está a escrever numa sala.
+Irá também ativar ou desativar o envio de notificações de escrita para outros.</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Sort rooms by unreads</source>
-        <translation type="unfinished"></translation>
+        <translation>Ordenar salas por não lidas</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Display rooms with new messages first.
 If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room.
 If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don&apos;t seem to consider them as important as the other rooms.</source>
-        <translation type="unfinished"></translation>
+        <translation>Exibe salas com novas mensagens primeiro.
+Se desativada, a lista de salas será apenas ordenada pela data da última mensagem de cada sala.
+Se ativada, salas com notificações ativas (pequeno círculo com um número dentro) serão ordenadas no topo. Salas silenciadas continuarão a ser ordenadas por data, visto que não são consideradas tão importantes como as outras.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
-        <translation type="unfinished"></translation>
+        <translation>Recibos de leitura</translation>
     </message>
     <message>
         <location line="+2"/>
         <source>Show if your message was read.
 Status is displayed next to timestamps.</source>
-        <translation type="unfinished"></translation>
+        <translation>Mostrar se a sua mensagem foi lida.
+Estado exibido ao lado da data.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
-        <translation type="unfinished"></translation>
+        <translation>Enviar mensagens como Markdown</translation>
     </message>
     <message>
         <location line="+2"/>
@@ -2187,6 +2701,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2227,12 +2746,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2242,7 +2796,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2317,7 +2871,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2337,17 +2891,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2362,12 +2921,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2387,7 +2941,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2409,22 +2963,22 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+54"/>
         <source>Select a file</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Selecionar um ficheiro</translation>
     </message>
     <message>
         <location line="+0"/>
         <source>All Files (*)</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Todos os ficheiros (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2432,19 +2986,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2459,6 +3013,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Não foi encontrada nenhuma conversa privada e encriptada com este utilizador. Crie uma e tente novamente.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2484,7 +3046,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+15"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Cancelar</translation>
     </message>
 </context>
 <context>
@@ -2502,12 +3064,12 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+23"/>
         <source>REGISTER</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">REGISTAR</translation>
     </message>
     <message>
         <location line="+5"/>
         <source>LOGIN</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">INCIAR SESSÃO</translation>
     </message>
 </context>
 <context>
@@ -2528,17 +3090,17 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+2"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Cancelar</translation>
     </message>
     <message>
         <location line="+10"/>
         <source>Name</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Nome</translation>
     </message>
     <message>
         <location line="+3"/>
         <source>Topic</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Tópico</translation>
     </message>
     <message>
         <location line="+3"/>
@@ -2571,7 +3133,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+1"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Cancelar</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2584,43 +3146,12 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
         <location filename="../../src/dialogs/Logout.cpp" line="+35"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Cancelar</translation>
     </message>
     <message>
         <location line="+8"/>
@@ -2638,7 +3169,7 @@ This usually causes the application icon in the task bar to animate in some fash
     <message>
         <location line="+1"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Cancelar</translation>
     </message>
     <message>
         <location line="+93"/>
@@ -2653,7 +3184,7 @@ Media size: %2
     <message>
         <location filename="../../src/dialogs/ReCaptcha.cpp" line="+35"/>
         <source>Cancel</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Cancelar</translation>
     </message>
     <message>
         <location line="+1"/>
@@ -2666,32 +3197,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3210,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2760,9 +3265,9 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">%1: %2</translation>
     </message>
     <message>
         <location line="+7"/>
@@ -2772,7 +3277,7 @@ Media size: %2
     <message>
         <location line="+3"/>
         <source>%1 sent an encrypted message</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">%1 enviou uma mensagem encriptada</translation>
     </message>
     <message>
         <location line="+5"/>
@@ -2780,27 +3285,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2808,7 +3313,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_ro.ts b/resources/langs/nheko_ro.ts
index 6ea496f10459c529b1b4782a507229b88e7ad34f..6d28da4689d0215e0db1126df43560d8b62bc807 100644
--- a/resources/langs/nheko_ro.ts
+++ b/resources/langs/nheko_ro.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation type="unfinished"></translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation type="unfinished"></translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Nu s-a putut invita utilizatorul: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Utilizator invitat: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Nu s-a putut muta cache-ul pe versiunea curentă. Acest lucru poate avea diferite cauze. Vă rugăm să deschideți un issue și încercați să folosiți o versiune mai veche între timp. O altă opțiune ar fi să încercați să ștergeți cache-ul manual.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Camera %1 a fost creată.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Nu s-a putut invita %1 în %2: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Utilizator eliminat: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Nu s-a putut interzice utilizatorul %1 în %2: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Nu s-a putut dezinterzice %1 în %2: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Utilizator dezinterzis: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Nu s-a putut migra cache-ul!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>Cache-ul de pe disc este mai nou decât versiunea pe care Nheko o suportă. Vă rugăm actualizați sau ștergeți cache-ul.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Nu s-a putut restabili contul OLM. Vă rugăm să vă reconectați.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Nu s-au putut restabili datele salvate. Vă rugăm să vă reconectați.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Nu s-au putut stabili cheile. Răspunsul serverului: %1 %2. Vă rugăm încercați mai târziu.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Vă rugăm să vă reconectați: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Nu s-a putut alătura la cameră: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>V-ați alăturat camerei</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Nu s-a putut șterge invitația: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Nu s-a putut crea camera: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Nu s-a putut părăsi camera: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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 filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished">ÃŽnchide</translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished">ÃŽnchide</translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished"></translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished">Toate fișierele (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">IDul camerei sau alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Părăsește camera</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Sigur vrei să părăsești camera?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -760,25 +885,25 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>CONECTARE</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Autodescoperirea a eșuat. Răspunsul primit este defectuos.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Autodescoperirea a eșuat. Eroare necunoscută la solicitarea .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Punctele finale necesare nu au fost găsite. Posibil a nu fi un server Matrix.</translation>
     </message>
@@ -788,30 +913,48 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>Răspuns eronat primit. Verificați ca domeniul homeserverului să fie valid.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>A apărut o eroare necunoscută. Verificați ca domeniul homeserverului să fie valid.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>CONECTARE SSO</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Parolă necompletată</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>Conectarea SSO a eșuat</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
@@ -822,7 +965,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>Criptare activată</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>numele camerei schimbat la: %1</translation>
     </message>
@@ -881,6 +1024,11 @@ Exemplu: https://serverul.meu:8787</translation>
         <source>Negotiating call...</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -905,7 +1053,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -928,7 +1076,7 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -948,17 +1096,19 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished">Opțiuni</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1017,6 +1167,11 @@ Exemplu: https://serverul.meu:8787</translation>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1076,33 +1236,29 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished">Acceptare</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation type="unfinished">%1 a trimis un mesaj criptat</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished">%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,7 +1289,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1164,7 +1320,7 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1179,21 +1335,37 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Confirmări de citire</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Nume de utilizator</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Numele de utilizator nu poate fi gol, și trebuie să conțină doar caracterele a-z, 0-9, ., =, - și /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Parolă</translation>
     </message>
@@ -1213,7 +1385,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>Homeserver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Un server care permite înregistrarea. Deoarece Matrix este decentralizat, trebuie să găsiți un server pe care să vă înregistrați sau să vă găzduiți propriul server.</translation>
     </message>
@@ -1223,27 +1395,17 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>ÃŽNREGISTRARE</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Fluxuri de înregistrare nesuportate!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished">Autodescoperirea a eșuat. Răspunsul primit este defectuos.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished">Autodescoperirea a eșuat. Eroare necunoscută la solicitarea .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished">Punctele finale necesare nu au fost găsite. Posibil a nu fi un server Matrix.</translation>
     </message>
@@ -1258,17 +1420,17 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished">A apărut o eroare necunoscută. Verificați ca domeniul homeserverului să fie valid.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Parola nu este destul de lungă (minim 8 caractere)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Parolele nu se potrivesc</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Nume server invalid</translation>
     </message>
@@ -1276,7 +1438,7 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished">ÃŽnchide</translation>
     </message>
@@ -1286,10 +1448,28 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation>nicio versiune stocată</translation>
     </message>
@@ -1297,7 +1477,7 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1306,16 +1486,6 @@ Exemplu: https://serverul.meu:8787</translation>
         <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1347,7 +1517,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1367,12 +1537,35 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished">Deconectare</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">ÃŽnchide</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished">Începe o nouă conversație</translation>
     </message>
@@ -1392,7 +1585,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished">Registru de camere</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished">Setări utilizator</translation>
     </message>
@@ -1400,12 +1593,12 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1419,16 +1612,36 @@ Exemplu: https://serverul.meu:8787</translation>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1458,7 +1671,12 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1473,7 +1691,17 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1519,12 +1747,12 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished">Nu s-a putut activa criptarea: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished">Selectează un avatar</translation>
     </message>
@@ -1544,8 +1772,8 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished">Eroare întâmpinată la citirea fișierului: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished">Nu s-a putut încărca imaginea: %s</translation>
     </message>
@@ -1553,21 +1781,49 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1622,6 +1878,121 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1674,18 +2045,18 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Redactare mesaj eșuată: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Salvați imaginea</translation>
     </message>
@@ -1705,7 +2076,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>Salvați fișier</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1715,7 +2086,7 @@ Exemplu: https://serverul.meu:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 a deschis camera publicului.</translation>
     </message>
@@ -1725,7 +2096,17 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>%1 a făcut ca această cameră să necesite o invitație pentru alăturare.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 a deschis camera pentru vizitatori.</translation>
     </message>
@@ -1745,12 +2126,12 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>%1 a făcut istoricul camerei accesibil membrilor din acest moment.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 a setat istoricul camerei accesibil participanților din momentul invitării.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 a setat istoricul camerei accesibil membrilor din momentul alăturării.</translation>
     </message>
@@ -1760,12 +2141,12 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>%1 a modificat permisiunile camerei.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 a fost invitat(ă).</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 și-a schimbat avatarul.</translation>
     </message>
@@ -1775,12 +2156,17 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>%1 și-a schimbat niște informații de pe profil.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 s-a alăturat.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 a respins invitația.</translation>
     </message>
@@ -1810,32 +2196,32 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>%1 a fost interzis(ă).</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 și-a redactat ciocănitul.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Te-ai alăturat camerei.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Ciocănit refuzat de la %1.</translation>
     </message>
@@ -1854,7 +2240,7 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1862,12 +2248,17 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Nicio cameră deschisă</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1892,28 +2283,40 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1951,10 +2354,35 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>Ieșire</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1964,33 +2392,98 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished">Selectează un avatar</translation>
     </message>
@@ -2013,8 +2506,8 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2022,7 +2515,7 @@ Exemplu: https://serverul.meu:8787</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Minimizează în bara de notificări</translation>
     </message>
@@ -2032,22 +2525,22 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation>Pornește în bara de notificări</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Bara laterală a grupului</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Avatare rotunde</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2072,7 +2565,7 @@ Exemplu: https://serverul.meu:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2087,6 +2580,16 @@ Exemplu: https://serverul.meu:8787</translation>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2115,7 +2618,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2170,7 +2673,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Confirmări de citire</translation>
     </message>
@@ -2181,7 +2684,7 @@ Status is displayed next to timestamps.</source>
         <translation>Vezi dacă mesajul tău a fost citit. Starea este afișată lângă timestampuri.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Trimite mesaje ca Markdown</translation>
     </message>
@@ -2193,6 +2696,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Notificări desktop</translation>
     </message>
@@ -2233,12 +2741,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Mărește fontul mesajelor dacă doar câteva emojiuri sunt afișate.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2248,7 +2791,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Factor de dimensiune</translation>
     </message>
@@ -2323,7 +2866,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Amprentă Dispozitiv</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Chei de sesiune</translation>
     </message>
@@ -2343,17 +2886,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>CRIPTARE</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>GENERAL</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>INTERFAȚĂ</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2368,12 +2916,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Familia de font pentru Emoji</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2393,7 +2936,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2423,14 +2966,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished">Toate fișierele (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Deschide fișierul de sesiuni</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2438,19 +2981,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Eroare</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Parolă fișier</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Introduceți parola pentru a decripta fișierul:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>Parola nu poate fi necompletată</translation>
     </message>
@@ -2465,6 +3008,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Fișier pentru salvarea cheilor de sesiune exportate</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2590,37 +3141,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Deschideți fallback, urmăriți pașii și confirmați după ce i-ați completat.</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Alăturare</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Anulare</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>IDul camerei sau alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Anulare</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Sigur vrei să părăsești camera?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2674,32 +3194,6 @@ Dimensiune media: %2
         <translation>Rezolvă reCAPTCHA și apasă butonul de confirmare</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Confirmări de citire</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>ÃŽnchide</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Azi %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Ieri %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2713,47 +3207,47 @@ Dimensiune media: %2
         <translation>%1 a trimis un clip audio</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Ai trimis o imagine</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 a trimis o imagine</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Ai trimis un fișier</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 a trimis un fișier</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Ai trimis un video</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 a trimis un video</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Ai trimis un sticker</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 a trimis un sticker</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Ai trimis o notificare</translation>
     </message>
@@ -2768,7 +3262,7 @@ Dimensiune media: %2
         <translation>Tu: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2788,27 +3282,27 @@ Dimensiune media: %2
         <translation>Ai inițiat un apel</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 a inițiat un apel</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Ai răspuns unui apel</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 a răspuns unui apel</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Ai încheiat un apel</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 a încheiat un apel</translation>
     </message>
@@ -2816,7 +3310,7 @@ Dimensiune media: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Tip mesaj necunoscut</translation>
     </message>
diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts
index 67a306f24e3aec6679a66db01fdbafc531698d04..698d277ff0817e59ccf52aa2d21b1b831072abe0 100644
--- a/resources/langs/nheko_ru.ts
+++ b/resources/langs/nheko_ru.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Вызов...</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Видео Звонок</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Видео Звонок</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation>Весь экран</translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Не удалось пригласить пользователя: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Приглашенный пользователь: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Миграция кэша для текущей версии не удалась. Это может происходить по разным причинам. Пожалуйста сообщите о проблеме и попробуйте временно использовать старую версию. Так-же вы можете попробовать удалить кэш самостоятельно.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation>Подтвердить вход</translation>
     </message>
@@ -151,23 +151,23 @@
         <translation>Вы действительно хотите присоединиться?</translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Комната %1 создана.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Подтвердите приглашение</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Вы точно хотите пригласить %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Не удалось пригласить %1 в %2: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Вы точно хотите выгнать %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Выгнанный пользователь: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation>Вы точно хотите заблокировать %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Не удалось заблокировать %1 в %2: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation>Вы точно хотите разблокировать %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Не удалось разблокировать %1 в %2: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Разблокированный пользователь: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation>Вы действительно хотите начать личную переписку с %1?</translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Миграция кэша не удалась!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>Ваш кэш новее, чем эта версия Nheko поддерживает. Пожалуйста обновитесь или отчистите ваш кэш.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Повторите попытку входа: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Не удалось присоединиться к комнате: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Вы присоединились к комнате</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Не удалось отменить приглашение: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Не удалось создать комнату: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Не удалось покинуть комнату: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation>Не удалось выгнать %1 из %2: %3</translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Расшифровать секреты</translation>
     </message>
@@ -362,12 +364,12 @@
         <translation>Введите свой ключ восстановления или пароль для расшифровки секретов: </translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation>Введите свой ключ восстановления или пароль названный %1 для расшифровки Ваших секретов:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Расшифровка не удалась</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Поиск</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Люди</translation>
     </message>
@@ -495,71 +497,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Это сообщение не зашифровано!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
-        <translation>Зашифровано верефицированым устройством</translation>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
-        <translation>Зашифрованно неверефицированым устройством, но Вы все еще доверяете этому пользователю.</translation>
+        <source>There was an internal error reading the decryption key from the database.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
-        <translation>Зашифровано неверифицированым устройства</translation>
+        <source>There was an error decrypting this message.</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>-- Зашифрованное событие (Не найдено ключей для дешифрования) --</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
-        <translation>-- Зашифрованное событие(Не найдено ключей для дешифрования) --</translation>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>-- Ошибка дешифрования (Не удалось получить megolm-ключи из бд) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Ошибка Дешифрования (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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>-- Шифрованое Событие (Неизвестный тип события) --</translation>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Это сообщение не зашифровано!</translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Атака повтором! Индекс этого сообщение был использован снова! --</translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation>Зашифровано верефицированым устройством</translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Сообщение от неверифицированного устройства! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation>Зашифрованно неверефицированым устройством, но Вы все еще доверяете этому пользователю.</translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Время для верификации устройста закончилось.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Другая сторона отменила верификацию.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Закрыть</translation>
     </message>
@@ -604,6 +613,81 @@
         <translation>Переслать Сообщение</translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished">Редактировать</translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished">Закрыть</translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Выберите файл</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation>Все файлы (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Не удалось загрузить медиа. Пожалуйста попробуйте ещё раз</translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Идентификатор или псевдоним комнаты</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Покинуть комнату</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Вы действительно желаете выйти?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -760,25 +885,25 @@ Example: https://server.my:8787</source>
         <translation>ВОЙТИ</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation>Вы ввели не правильный Matrix ID, @joe:matrix.org</translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Автообноружение не удалось. Получен поврежденный ответ.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Автообноружение не удалось. Не известаня ошибка во время запроса .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Необходимые конечные точки не найдены. Возможно, это не сервер Matrix.</translation>
     </message>
@@ -788,30 +913,48 @@ Example: https://server.my:8787</source>
         <translation>Получен неверный ответ. Убедитесь, что домен homeserver действителен.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Произошла неизвестная ошибка. Убедитесь, что домен homeserver действителен.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>SSO ВХОД</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Пустой пароль</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>SSO вход не удался</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>убрано</translation>
@@ -822,7 +965,7 @@ Example: https://server.my:8787</source>
         <translation>Шифрование включено</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>имя комнаты изменено на: %1</translation>
     </message>
@@ -881,6 +1024,11 @@ Example: https://server.my:8787</source>
         <source>Negotiating call...</source>
         <translation>Совершение звонка...</translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -905,7 +1053,7 @@ Example: https://server.my:8787</source>
         <translation>Написать сообщение…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -928,7 +1076,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation>Редактировать</translation>
     </message>
@@ -948,17 +1096,19 @@ Example: https://server.my:8787</source>
         <translation>Опции</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1017,6 +1167,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Example: https://server.my:8787</source>
         <translation>Получен Запрос Верификации</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1076,33 +1236,29 @@ Example: https://server.my:8787</source>
         <translation>Принять</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation>%1 отправил зашифрованное сообщение</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished">%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,7 +1289,7 @@ Example: https://server.my:8787</source>
         <translation>Микрофон не найден.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Голос</translation>
     </message>
@@ -1164,7 +1320,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation>Создать уникальный профиль, который позволяет вести несколько аккаунтов и запускать множество сущностей nheko. </translation>
     </message>
@@ -1179,21 +1335,37 @@ Example: https://server.my:8787</source>
         <translation>имя профиля</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Просмотр получателей</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Имя пользователя</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Имя пользователя не должно быть пустым и должно содержать только символы a-z, 0-9, ., _, =, -, и /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Пароль</translation>
     </message>
@@ -1213,7 +1385,7 @@ Example: https://server.my:8787</source>
         <translation>Домашний сервер</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>Сервер разрешающий регистрацию.Поскольку matrix децентрализованный, нужно выбрать сервер где вы можете зарегистрироваться или поднимите свой сервер.</translation>
     </message>
@@ -1223,27 +1395,17 @@ Example: https://server.my:8787</source>
         <translation>РЕГИСТРАЦИЯ</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Нет поддреживаемых регистрационных потоков</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>Одно или более полей имеют некорректный ввод. Пожалуйста устраните ошибки и попробуйте снова.</translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished">Автообноружение не удалось. Получен поврежденный ответ.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished">Автообноружение не удалось. Не известаня ошибка во время запроса .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished">Необходимые конечные точки не найдены. Возможно, это не сервер Matrix.</translation>
     </message>
@@ -1258,17 +1420,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Произошла неизвестная ошибка. Убедитесь, что домен homeserver действителен.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Слишком короткий пароль (минимум 8 символов)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Пароли не совпадают</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Неверное имя сервера</translation>
     </message>
@@ -1276,7 +1438,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Закрыть</translation>
     </message>
@@ -1287,33 +1449,41 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
-        <translation>нет сохраненной версии</translation>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>RoomInfo</name>
     <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation>нет сохраненной версии</translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1347,7 +1517,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1367,12 +1537,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished">Выйти</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Закрыть</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished">Начать новый чат</translation>
     </message>
@@ -1392,7 +1585,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Каталог комнат</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished">Пользовательские настройки</translation>
     </message>
@@ -1400,12 +1593,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1419,16 +1612,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation>Настройки комнаты</translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation>%1 участник(ов)</translation>
     </message>
@@ -1458,7 +1671,12 @@ Example: https://server.my:8787</source>
         <translation>Все сообщения</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation>Каждый и гости</translation>
     </message>
@@ -1473,7 +1691,17 @@ Example: https://server.my:8787</source>
         <translation>Приглашённые пользователи</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation>Шифрование</translation>
     </message>
@@ -1519,12 +1747,12 @@ Example: https://server.my:8787</source>
         <translation>Версия Комнаты</translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation>Не удалось включить шифрование: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation>Выберите аватар</translation>
     </message>
@@ -1544,8 +1772,8 @@ Example: https://server.my:8787</source>
         <translation>Ошибка во время прочтения файла: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation>Не удалось загрузить изображение: %s</translation>
     </message>
@@ -1553,21 +1781,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1622,6 +1878,121 @@ Example: https://server.my:8787</source>
         <translation>Отмена</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1674,18 +2045,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Ошибка редактирования сообщения: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation>Не удалось зашифровать сообщение, отправка отменена!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Сохранить изображение</translation>
     </message>
@@ -1705,7 +2076,7 @@ Example: https://server.my:8787</source>
         <translation>Сохранить файл</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1715,7 +2086,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 сделал комнату публичной.</translation>
     </message>
@@ -1725,7 +2096,17 @@ Example: https://server.my:8787</source>
         <translation>%1 сделал вход в комнату по приглашению.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 сделал комнату открытой для гостей.</translation>
     </message>
@@ -1745,12 +2126,12 @@ Example: https://server.my:8787</source>
         <translation>%1 сделал историю сообщений видимо для участников с этого момента.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 сделал историю сообщений видимой для участников, с момента их приглашения.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 сделал историю сообщений видимой для участников, с момента того, как они присоединились к комнате.</translation>
     </message>
@@ -1760,12 +2141,12 @@ Example: https://server.my:8787</source>
         <translation>%1 поменял разрешения для комнаты.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 был приглашен.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 поменял свой аватар.</translation>
     </message>
@@ -1775,12 +2156,17 @@ Example: https://server.my:8787</source>
         <translation>%1 поменял информацию в профиле.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 присоединился.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 отклонил приглашение.</translation>
     </message>
@@ -1810,32 +2196,32 @@ Example: https://server.my:8787</source>
         <translation>%1 был заблокирован.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 отредактировал его &quot;стук&quot;.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Вы присоединились к этой комнате.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Отверг &quot;стук&quot; от %1</translation>
     </message>
@@ -1854,7 +2240,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation>Изменено</translation>
     </message>
@@ -1862,12 +2248,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Комната не выбрана</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished">%1 участник(ов)</translation>
     </message>
@@ -1892,28 +2283,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">Вернуться к списку комнат</translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation>Не найдено личного чата с этим пользователем. Создайте зашифрованный личный чат с этим пользователем и попытайтесь еще раз.</translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation>Вернуться к списку комнат</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation>Комнаты не выбраны</translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation>Настройки комнаты</translation>
     </message>
@@ -1951,10 +2354,35 @@ Example: https://server.my:8787</source>
         <translation>Выйти</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation>Глобальный Пользовательский Профиль</translation>
     </message>
@@ -1964,33 +2392,98 @@ Example: https://server.my:8787</source>
         <translation>Поользовательский Профиль в Комнате</translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
         <translation>Верифицировать</translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
-        <translation>Заблокировать пользователя</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation>Создать приватный чат</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
-        <translation>Выгнать пользователя</translation>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation>Отменить Верификацию</translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Выберите аватар</translation>
     </message>
@@ -2013,8 +2506,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation>По умолчанию</translation>
     </message>
@@ -2022,7 +2515,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Сворачивать в системную панель</translation>
     </message>
@@ -2032,22 +2525,22 @@ Example: https://server.my:8787</source>
         <translation>Запускать в системной панели</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Боковая панель групп</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Округлый Аватар</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation>профиль: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation>По умолчанию</translation>
     </message>
@@ -2072,7 +2565,7 @@ Example: https://server.my:8787</source>
         <translation>СКАЧАТЬ</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation>Держать приложение запущенным в фоне, после закрытия окна.</translation>
     </message>
@@ -2087,6 +2580,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation>Поменять отображение пользовательского аватара в чатах. ВЫКЛ - квадратный, ВКЛ - округлый.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2115,7 +2618,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2172,7 +2675,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
 Если это включено, комнаты в которых включены уведомления (маленькие кружки с числами) буду отсортированы на верху. Комнаты, которые вы заглушили, будут отсортированы по времени, пока вы не сделаете их важнее чем другие комнаты.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Просмотр получателей</translation>
     </message>
@@ -2184,7 +2687,7 @@ Status is displayed next to timestamps.</source>
 Стату отображается за временем сообщения.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Посылать сообщение в формате Markdown</translation>
     </message>
@@ -2197,6 +2700,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Уведомления на рабочем столе</translation>
     </message>
@@ -2238,12 +2746,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Делать шрифт больше, если сообщения содержать только несколько эмоджи.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation>Делиться ключами с проверенными участниками и устройствами</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation>Закешировано</translation>
     </message>
@@ -2253,7 +2796,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>НЕ ЗАКЕШИРОВАНО</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Масштаб</translation>
     </message>
@@ -2328,7 +2871,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Отпечаток устройства</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Ключи сеанса</translation>
     </message>
@@ -2348,17 +2891,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>ШИФРОВАНИЕ</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>ГЛАВНОЕ</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>ИНТЕРФЕЙС</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation>Сенсорный режим</translation>
     </message>
@@ -2373,12 +2921,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Семья шрифта эмоджи</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation>Автоматически отвечать на запросы ключей от других пользователей, если они верифицированы.</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2398,7 +2941,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2428,14 +2971,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Все файлы (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Открыть файл сеансов</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2443,20 +2986,20 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Ошибка</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translatorcomment>Или введите пароль?</translatorcomment>
         <translation>Пароль файла</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Введите парольную фразу для расшифрования файла:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>Пароль не может быть пустым</translation>
     </message>
@@ -2471,6 +3014,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Файл для сохранения экспортированных ключей сеанса</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Не найдено личного чата с этим пользователем. Создайте зашифрованный личный чат с этим пользователем и попытайтесь еще раз.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2596,37 +3147,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>Запустите резервный вариант, пройдите его шаги и подтвердите завершение.</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>Присоединиться</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Отмена</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Идентификатор или псевдоним комнаты</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Отмена</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Вы действительно желаете выйти?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2680,32 +3200,6 @@ Media size: %2
         <translation>Решите reCAPTCHA и нажмите кнопку подтверждения</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Просмотреть получателей</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Закрыть</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Сегодня %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Вчера %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2719,47 +3213,47 @@ Media size: %2
         <translation>%1 отправил аудио запись</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Вы отправили картинку</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 отправил картинку</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Вы отправили файл</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 отправил файл</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Вы отправили видео</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 отправил видео</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Вы отправили стикер</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 отправил стикер</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Вы отправили оповещение</translation>
     </message>
@@ -2774,7 +3268,7 @@ Media size: %2
         <translation>Ð’Ñ‹: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2794,27 +3288,27 @@ Media size: %2
         <translation>Вы начали звонок</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 начал звонок</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Вы ответили на звонок</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 ответил на звонок</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Вы закончили разговор</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 Закончил разговор</translation>
     </message>
@@ -2822,7 +3316,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Неизвестный Тип Сообщения</translation>
     </message>
diff --git a/resources/langs/nheko_si.ts b/resources/langs/nheko_si.ts
index cf42599093d15241a6e6ade1c9901a6be644d467..579ca1cf063aca0360b442376c5fece60160a15e 100644
--- a/resources/langs/nheko_si.ts
+++ b/resources/langs/nheko_si.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation type="unfinished"></translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation type="unfinished"></translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -182,7 +182,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -227,12 +227,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation type="unfinished"></translation>
     </message>
@@ -247,33 +247,35 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation type="unfinished"></translation>
     </message>
@@ -283,7 +285,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -293,7 +295,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
     <message>
-        <location line="-52"/>
-        <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 filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished"></translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -756,25 +881,25 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -784,30 +909,48 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
@@ -818,7 +961,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -877,6 +1020,11 @@ Example: https://server.my:8787</source>
         <source>Negotiating call...</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -901,7 +1049,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -924,7 +1072,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1073,32 +1233,28 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,27 +1391,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1283,33 +1445,41 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+</context>
+<context>
+    <name>RoomInfo</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1414,16 +1607,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1453,7 +1666,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1468,7 +1686,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1514,12 +1742,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1539,8 +1767,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1548,21 +1776,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1617,6 +1873,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1669,18 +2040,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1700,7 +2071,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation type="unfinished">
@@ -1709,7 +2080,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1719,7 +2090,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1739,12 +2120,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1754,12 +2135,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1769,12 +2150,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1804,32 +2190,32 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1848,7 +2234,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1856,12 +2242,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1886,28 +2277,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1945,10 +2348,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1958,33 +2386,98 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+29"/>
+        <source>Room: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2007,8 +2500,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2016,7 +2509,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2026,22 +2519,22 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2066,7 +2559,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2081,6 +2574,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2109,7 +2612,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2175,7 +2678,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2666,32 +3186,6 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3199,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2760,7 +3254,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2780,27 +3274,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2808,7 +3302,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/langs/nheko_sv.ts b/resources/langs/nheko_sv.ts
index 25db1c4b601347443bb75364e42d52b425142509..e19136dbb255dfbbdea2a716f5e629c93666cc6e 100644
--- a/resources/langs/nheko_sv.ts
+++ b/resources/langs/nheko_sv.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation>Ringer upp...</translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation>Videosamtal</translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation>Videosamtal</translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation type="unfinished"></translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>Kunde inte bjuda in användare: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>Bjöd in användare: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>Kunde inte migrera cachen till den nuvarande versionen. Detta kan bero på flera anledningar, vänligen rapportera problemet och prova en äldre version under tiden. Du kan också försöka att manuellt radera cachen.</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>Rum %1 skapat.</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation>Bekräfta inbjudan</translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation>Är du säker på att du vill bjuda in %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation>Kunde inte bjuda in %1 till %2: %3</translation>
     </message>
@@ -182,7 +182,7 @@
         <translation>Är du säker på att du vill sparka ut %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>Sparkade ut användare: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation>Är du säker på att du vill bannlysa %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation>Kunde inte bannlysa %1 i %2: %3</translation>
     </message>
@@ -217,7 +217,7 @@
         <translation>Är du säker på att du vill häva bannlysningen av %1 (%2)?</translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation>Kunde inte häva bannlysningen av %1 i %2: %3</translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>Hävde bannlysningen av användare: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>Cache-migration misslyckades!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>Cachen på ditt lagringsmedia är nyare än vad denna version av Nheko stödjer. Vänligen uppdatera eller rensa din cache.</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>Kunde inte återställa OLM-konto. Vänligen logga in på nytt.</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>Kunde inte återställa sparad data. Vänligen logga in på nytt.</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>Kunde inte sätta upp krypteringsnycklar. Svar från servern: %1 %2. Vänligen försök igen senare.</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>Vänligen försök logga in på nytt: %1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>Kunde inte gå med i rum: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>Du gick med i rummet</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>Kunde inte ta bort inbjudan: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>Kunde inte skapa rum: %1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>Kunde inte lämna rum: %1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation>Dekryptera hemliga nycklar</translation>
     </message>
@@ -362,12 +364,12 @@
         <translation>Ange din återställningsnyckel eller lösenfras för att dekryptera dina hemliga nycklar:</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation>Ange din återställningsnyckel eller lösenfras vid namn %1 för att dekryptera dina hemliga nycklar:</translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation>Dekryptering misslyckades</translation>
     </message>
@@ -431,7 +433,7 @@
         <translation>Sök</translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation>Personer</translation>
     </message>
@@ -495,71 +497,69 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>Detta meddelande är inte krypterat!</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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>-- Krypterat Event (Inga nycklar kunde hittas för dekryptering) --</translation>
+        <location line="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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>-- Dekrypteringsfel (Kunde inte hämta megolm-nycklar från databas) --</translation>
+        <location line="+2"/>
+        <source>Unknown decryption error</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
-        <translation>-- Dekrypteringsfel (%1) --</translation>
+        <location line="+10"/>
+        <source>Request key</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>Detta meddelande är inte krypterat!</translation>
     </message>
     <message>
-        <location line="-52"/>
-        <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>-- Krypterat Event (Okänd eventtyp) --</translation>
+        <location line="+4"/>
+        <source>Encrypted by a verified device</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
-        <translation>-- Replay-attack! Detta meddelandeindex har blivit återanvänt! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
-        <translation>-- Meddelande från overifierad enhet! --</translation>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
+        <translation type="unfinished"></translation>
     </message>
 </context>
 <context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation>Enhetsverifikation tog för lång tid.</translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation>Motparten avbröt verifikationen.</translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation>Stäng</translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">Avbryt</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished">Stäng</translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation>Välj en fil</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation>Alla Filer (*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation>Kunde inte ladda upp media. Vänligen försök igen.</translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">Avbryt</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">Rum-ID eller alias</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">Lämna rum</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">Är du säker på att du vill lämna?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -760,25 +885,25 @@ Exempel: https://server.my:8787</translation>
         <translation>INLOGGNING</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation>Autouppslag misslyckades. Mottog felkonstruerat svar.</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation>Autouppslag misslyckades. Okänt fel uppstod vid begäran av .well-known.</translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>Kunde inte hitta de nödvändiga ändpunkterna. Möjligtvis inte en Matrix-server.</translation>
     </message>
@@ -788,35 +913,53 @@ Exempel: https://server.my:8787</translation>
         <translation>Mottog felkonstruerat svar. Se till att hemserver-domänen är giltig.</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>Ett okänt fel uppstod. Se till att hemserver-domänen är giltig.</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation>SSO INLOGGNING</translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>Tomt lösenord</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation>SSO-inloggning misslyckades</translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+187"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+192"/>
         <source>Encryption enabled</source>
         <translation>Kryptering aktiverad</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation>rummets namn ändrat till: %1</translation>
     </message>
@@ -866,18 +1009,23 @@ Exempel: https://server.my:8787</translation>
         <translation>Förhandlar samtal…</translation>
     </message>
     <message>
-        <location line="-24"/>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-94"/>
         <source>%1 answered the call.</source>
         <translation>%1 besvarade samtalet.</translation>
     </message>
     <message>
-        <location line="-99"/>
+        <location line="-109"/>
         <location line="+9"/>
         <source>removed</source>
         <translation>borttagen</translation>
     </message>
     <message>
-        <location line="+102"/>
+        <location line="+112"/>
         <source>%1 ended the call.</source>
         <translation>%1 avslutade samtalet.</translation>
     </message>
@@ -905,7 +1053,7 @@ Exempel: https://server.my:8787</translation>
         <translation>Skriv ett meddelande…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -928,7 +1076,7 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -948,17 +1096,19 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished">Alternativ</translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1017,6 +1167,11 @@ Exempel: https://server.my:8787</translation>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1031,7 +1186,12 @@ Exempel: https://server.my:8787</translation>
         <translation>Mottog Verifikationsförfrågan</translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation>Du kan verifiera dina enheter för att låta andra användare se vilka av dem som faktiskt tillhör dig. Detta gör även att nyckelbackup fungerar automatiskt. Verifiera %1 nu?</translation>
     </message>
@@ -1076,33 +1236,29 @@ Exempel: https://server.my:8787</translation>
         <translation>Godkänn</translation>
     </message>
 </context>
+<context>
+    <name>NotificationWarning</name>
+    <message>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>NotificationsManager</name>
     <message>
         <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
         <source>%1 sent an encrypted message</source>
         <translation type="unfinished">%1 skickade ett krypterat meddelande</translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished">%1: %2</translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1133,7 +1289,7 @@ Exempel: https://server.my:8787</translation>
         <translation>Ingen mikrofon kunde hittas.</translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation>Röst</translation>
     </message>
@@ -1164,7 +1320,7 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation>Skapa en unik profil, vilket tillåter dig att logga in på flera konton samtidigt och starta flera instanser av Nheko.</translation>
     </message>
@@ -1179,21 +1335,37 @@ Exempel: https://server.my:8787</translation>
         <translation>profilnamn</translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">Läskvitton</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>Användarnamn</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation>Användarnamnet kan inte vara tomt, och måste enbart innehålla tecknen a-z, 0-9, ., _, =, -, och /.</translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>Lösenord</translation>
     </message>
@@ -1213,7 +1385,7 @@ Exempel: https://server.my:8787</translation>
         <translation>Hemserver</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation>En server som tillåter registrering. Eftersom matrix är decentraliserat behöver du först hitta en server du kan registrera dig på, eller upprätta en på egen hand.</translation>
     </message>
@@ -1223,27 +1395,17 @@ Exempel: https://server.my:8787</translation>
         <translation>REGISTRERA</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation>Inga stödda registreringsflöden!</translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation>Ett eller flera fält har ogiltigt innehåll. Vänligen korrigera problemen och försök igen.</translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished">Autouppslag misslyckades. Mottog felkonstruerat svar.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished">Autouppslag misslyckades. Okänt fel uppstod vid begäran av .well-known.</translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished">Kunde inte hitta de nödvändiga ändpunkterna. Möjligtvis inte en Matrix-server.</translation>
     </message>
@@ -1258,17 +1420,17 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished">Ett okänt fel uppstod. Se till att hemserver-domänen är giltig.</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>Lösenordet är inte långt nog (minst 8 tecken)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>Lösenorden stämmer inte överens</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>Ogiltigt servernamn</translation>
     </message>
@@ -1276,7 +1438,7 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation>Stäng</translation>
     </message>
@@ -1287,33 +1449,41 @@ Exempel: https://server.my:8787</translation>
     </message>
 </context>
 <context>
-    <name>RoomInfo</name>
+    <name>RoomDirectory</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
-        <source>no version stored</source>
-        <translation>ingen version lagrad</translation>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
-        <source>New tag</source>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+1"/>
-        <source>Enter the tag you want to use:</source>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>RoomInfo</name>
     <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
+        <source>no version stored</source>
+        <translation>ingen version lagrad</translation>
+    </message>
+</context>
+<context>
+    <name>RoomList</name>
+    <message>
+        <location filename="../qml/RoomList.qml" line="+67"/>
+        <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
+        <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1347,7 +1517,7 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1367,12 +1537,35 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished">Logga ut</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished">Stäng</translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished">Starta en ny chatt</translation>
     </message>
@@ -1392,7 +1585,7 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished">Rumkatalog</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished">Användarinställningar</translation>
     </message>
@@ -1400,12 +1593,12 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1418,16 +1611,36 @@ Exempel: https://server.my:8787</translation>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1457,7 +1670,12 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1472,7 +1690,17 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1518,12 +1746,12 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished">Kunde inte aktivera kryptering: %1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished">Välj en avatar</translation>
     </message>
@@ -1543,8 +1771,8 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished">Kunde inte läsa filen: %1</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished">Kunde inte ladda upp bilden: %s</translation>
     </message>
@@ -1552,21 +1780,49 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1621,6 +1877,121 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished">Avbryt</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1673,18 +2044,18 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation>Kunde inte maskera meddelande: %1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation>Kunde inte kryptera event, sändning avbruten!</translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation>Spara bild</translation>
     </message>
@@ -1704,7 +2075,7 @@ Exempel: https://server.my:8787</translation>
         <translation>Spara fil</translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation>
@@ -1713,7 +2084,7 @@ Exempel: https://server.my:8787</translation>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation>%1 öppnade rummet till allmänheten.</translation>
     </message>
@@ -1723,7 +2094,17 @@ Exempel: https://server.my:8787</translation>
         <translation>%1 satte rummet till att kräva inbjudan för att gå med.</translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation>%1 öppnade rummet för gäståtkomst.</translation>
     </message>
@@ -1743,12 +2124,12 @@ Exempel: https://server.my:8787</translation>
         <translation>%1 gjorde rummets historik läslig för medlemmar från och med nu.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation>%1 gjorde rummets historik läslig för medlemmar sedan de blev inbjudna.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation>%1 gjorde rummets historik läslig för medlemmar sedan de gick med i rummet.</translation>
     </message>
@@ -1758,12 +2139,12 @@ Exempel: https://server.my:8787</translation>
         <translation>%1 har ändrat rummets behörigheter.</translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation>%1 blev inbjuden.</translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation>%1 ändrade sin avatar.</translation>
     </message>
@@ -1773,12 +2154,17 @@ Exempel: https://server.my:8787</translation>
         <translation>%1 ändrade någon profilinfo.</translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation>%1 gick med.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation>%1 avvisade sin inbjudan.</translation>
     </message>
@@ -1808,32 +2194,32 @@ Exempel: https://server.my:8787</translation>
         <translation>%1 blev bannlyst.</translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation>%1 maskerade sin knackning.</translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation>Du gick med i detta rum.</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation>Avvisade knackningen från %1.</translation>
     </message>
@@ -1852,7 +2238,7 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1860,12 +2246,17 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation>Inget rum öppet</translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1890,28 +2281,40 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished">Tillbaka till rumlista</translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation>Ingen krypterad privat chatt med denna användare kunde hittas. Skapa en krypterad privat chatt med användaren och försök igen.</translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation>Tillbaka till rumlista</translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation>Inget rum markerat</translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation>Alternativ för rum</translation>
     </message>
@@ -1949,10 +2352,35 @@ Exempel: https://server.my:8787</translation>
         <translation>Avsluta</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1962,33 +2390,98 @@ Exempel: https://server.my:8787</translation>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
         <source>Verify</source>
         <translation>Bekräfta</translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
-        <translation>Bannlys användaren</translation>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
-        <translation>Starta en privat chatt</translation>
+        <location line="+8"/>
+        <source>Kick the user.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
-        <source>Kick the user</source>
-        <translation>Sparka ut användaren</translation>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation>Välj en avatar</translation>
     </message>
@@ -2011,8 +2504,8 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2020,7 +2513,7 @@ Exempel: https://server.my:8787</translation>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>Minimera till systemtråg</translation>
     </message>
@@ -2030,22 +2523,22 @@ Exempel: https://server.my:8787</translation>
         <translation>Starta i systemtråg</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>Gruppens sidofält</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation>Cirkulära avatarer</translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation>profil: %1</translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2070,7 +2563,7 @@ Exempel: https://server.my:8787</translation>
         <translation>LADDA NED</translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation>Håll applikationen igång i bakgrunden efter att ha stängt klienten.</translation>
     </message>
@@ -2086,6 +2579,16 @@ OFF - square, ON - Circle.</source>
         <translation>Ändra utseendet av användaravatarer i chattar.
 AV - Kvadrat, PÃ… - Cirkel.</translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2115,7 +2618,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2173,7 +2676,7 @@ Om denna inställning är av kommer listan över rum endast sorteras efter när
 Om denna inställning är på kommer rum med aktiva notifikationer (den lilla cirkeln med ett nummer i) sorteras högst upp. Rum som du stängt av notifikationer för kommer fortfarande sorteras efter när det sista meddelandet skickades, eftersom du inte verkar tycka att de är lika viktiga som andra rum.</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>Läskvitton</translation>
     </message>
@@ -2185,7 +2688,7 @@ Status is displayed next to timestamps.</source>
 Status visas bredvid tidsstämpel.</translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation>Skicka meddelanden som Markdown</translation>
     </message>
@@ -2198,6 +2701,11 @@ Om denna inställning är av kommer alla meddelanden skickas som oformatterad te
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>Skrivbordsnotifikationer</translation>
     </message>
@@ -2239,12 +2747,47 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Öka fontstorleken på meddelanden som enbart innehåller ett par emoji.</translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation>Dela nycklar med verifierade användare och enheter</translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation>SPARAD</translation>
     </message>
@@ -2254,7 +2797,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>EJ SPARAD</translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation>Storleksfaktor</translation>
     </message>
@@ -2329,7 +2872,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Enhetsfingeravtryck</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>Sessionsnycklar</translation>
     </message>
@@ -2349,17 +2892,22 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>KRYPTERING</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>ALLMÄNT</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation>GRÄNSSNITT</translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation>Touchskärmsläge</translation>
     </message>
@@ -2374,12 +2922,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Emoji font-familj</translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation>Svarar automatiskt på nyckelförfrågningar från andra användare om de är verifierade.</translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation>Primär signeringsnyckel</translation>
     </message>
@@ -2399,7 +2942,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Nyckeln för att verifiera andra användare. Om den är sparad lokalt, kommer alla enheter tillhörande en användare verifieras när användaren verifieras.</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation>Självsigneringsnyckel</translation>
     </message>
@@ -2429,14 +2972,14 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Alla Filer (*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>Öppna sessionsfil</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2444,19 +2987,19 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Fel</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>Fillösenord</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>Ange lösenfrasen för att dekryptera filen:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>Lösenordet kan inte vara tomt</translation>
     </message>
@@ -2471,6 +3014,14 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Fil för att spara de exporterade sessionsnycklarna</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished">Ingen krypterad privat chatt med denna användare kunde hittas. Skapa en krypterad privat chatt med användaren och försök igen.</translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2596,37 +3147,6 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.<
         <translation>Öppna reserven, följ stegen och bekräfta när du slutfört dem.</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation>GÃ¥ med</translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>Avbryt</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>Rum-ID eller alias</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>Avbryt</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>Är du säker på att du vill lämna?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2680,32 +3200,6 @@ Mediastorlek: %2
         <translation>Lös reCAPTCHAn och tryck på Bekräfta</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>Läskvitton</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation>Stäng</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation>Idag %1</translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation>Igår %1</translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2719,47 +3213,47 @@ Mediastorlek: %2
         <translation>%1 skickade ett ljudklipp</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation>Du skickade en bild</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation>%1 skickade en bild</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation>Du skickade en fil</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation>%1 skickade en fil</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation>Du skickade en video</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation>%1 skickade en video</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation>Du skickade en sticker</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation>%1 skickade en sticker</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation>Du skickade en notis</translation>
     </message>
@@ -2774,7 +3268,7 @@ Mediastorlek: %2
         <translation>Du: %1</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation>%1: %2</translation>
     </message>
@@ -2794,27 +3288,27 @@ Mediastorlek: %2
         <translation>Du ringde upp</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation>%1 ringde upp</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation>Du besvarade ett samtal</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation>%1 besvarade ett samtal</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation>Du lade på</translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation>%1 lade på</translation>
     </message>
@@ -2822,7 +3316,7 @@ Mediastorlek: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation>Okänd meddelandetyp</translation>
     </message>
diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts
index 75e9db9505dbffac9bb6a38016237f1f0afa498e..36cfb0a039ee00cca22777b10321cb3da30352e3 100644
--- a/resources/langs/nheko_zh_CN.ts
+++ b/resources/langs/nheko_zh_CN.ts
@@ -4,7 +4,7 @@
 <context>
     <name>ActiveCallBar</name>
     <message>
-        <location filename="../qml/voip/ActiveCallBar.qml" line="+106"/>
+        <location filename="../qml/voip/ActiveCallBar.qml" line="+107"/>
         <source>Calling...</source>
         <translation type="unfinished"></translation>
     </message>
@@ -56,7 +56,7 @@
 <context>
     <name>CallInvite</name>
     <message>
-        <location filename="../qml/voip/CallInvite.qml" line="+70"/>
+        <location filename="../qml/voip/CallInvite.qml" line="+72"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -74,7 +74,7 @@
 <context>
     <name>CallInviteBar</name>
     <message>
-        <location filename="../qml/voip/CallInviteBar.qml" line="+64"/>
+        <location filename="../qml/voip/CallInviteBar.qml" line="+65"/>
         <source>Video Call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -117,7 +117,7 @@
 <context>
     <name>CallManager</name>
     <message>
-        <location filename="../../src/CallManager.cpp" line="+521"/>
+        <location filename="../../src/voip/CallManager.cpp" line="+513"/>
         <source>Entire screen</source>
         <translation type="unfinished"></translation>
     </message>
@@ -125,23 +125,23 @@
 <context>
     <name>ChatPage</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+135"/>
+        <location filename="../../src/ChatPage.cpp" line="+126"/>
         <source>Failed to invite user: %1</source>
         <translation>邀请用户失败: %1</translation>
     </message>
     <message>
         <location line="+4"/>
-        <location line="+665"/>
+        <location line="+662"/>
         <source>Invited user: %1</source>
         <translation>邀请已发送: %1</translation>
     </message>
     <message>
-        <location line="-456"/>
+        <location line="-448"/>
         <source>Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually.</source>
         <translation>无法迁移缓存到目前版本,可能有多种原因引发此类问题。您可以新建一个议题并继续使用之前版本,或者您可以尝试手动删除缓存。</translation>
     </message>
     <message>
-        <location line="+360"/>
+        <location line="+355"/>
         <source>Confirm join</source>
         <translation type="unfinished"></translation>
     </message>
@@ -151,23 +151,23 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+44"/>
+        <location line="+42"/>
         <source>Room %1 created.</source>
         <translation>房间“%1”已创建</translation>
     </message>
     <message>
         <location line="+34"/>
-        <location line="+286"/>
+        <location line="+445"/>
         <source>Confirm invite</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-285"/>
+        <location line="-444"/>
         <source>Do you really want to invite %1 (%2)?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to invite %1 to %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -182,7 +182,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+16"/>
+        <location line="+15"/>
         <source>Kicked user: %1</source>
         <translation>踢出用户: %1</translation>
     </message>
@@ -197,7 +197,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to ban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -217,7 +217,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+11"/>
+        <location line="+10"/>
         <source>Failed to unban %1 in %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -227,12 +227,12 @@
         <translation>解禁用户: %1</translation>
     </message>
     <message>
-        <location line="+189"/>
+        <location line="+352"/>
         <source>Do you really want to start a private chat with %1?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-727"/>
+        <location line="-879"/>
         <source>Cache migration failed!</source>
         <translation>缓存迁移失败!</translation>
     </message>
@@ -247,33 +247,35 @@
         <translation>本地缓存版本比现用的Nheko版本新。请升级Nheko或手动清除缓存。</translation>
     </message>
     <message>
-        <location line="+48"/>
+        <location line="+49"/>
         <source>Failed to restore OLM account. Please login again.</source>
         <translation>恢复 OLM 账户失败。请重新登录。</translation>
     </message>
     <message>
+        <location line="+4"/>
+        <location line="+4"/>
         <location line="+4"/>
         <source>Failed to restore save data. Please login again.</source>
         <translation>恢复保存的数据失败。请重新登录。</translation>
     </message>
     <message>
-        <location line="+101"/>
+        <location line="+93"/>
         <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source>
         <translation>设置密钥失败。服务器返回信息: %1 %2。请稍后再试。</translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+117"/>
+        <location line="+32"/>
+        <location line="+115"/>
         <source>Please try to login again: %1</source>
         <translation>请尝试再次登录:%1</translation>
     </message>
     <message>
-        <location line="+51"/>
+        <location line="+49"/>
         <source>Failed to join room: %1</source>
         <translation>无法加入房间: %1</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You joined the room</source>
         <translation>您已加入房间</translation>
     </message>
@@ -283,7 +285,7 @@
         <translation>无法移除邀请: %1</translation>
     </message>
     <message>
-        <location line="+21"/>
+        <location line="+20"/>
         <source>Room creation failed: %1</source>
         <translation>创建聊天室失败:%1</translation>
     </message>
@@ -293,7 +295,7 @@
         <translation>离开聊天室失败:%1</translation>
     </message>
     <message>
-        <location line="+60"/>
+        <location line="+58"/>
         <source>Failed to kick %1 from %2: %3</source>
         <translation type="unfinished"></translation>
     </message>
@@ -352,7 +354,7 @@
 <context>
     <name>CrossSigningSecrets</name>
     <message>
-        <location filename="../../src/ChatPage.cpp" line="+189"/>
+        <location filename="../../src/ChatPage.cpp" line="+288"/>
         <source>Decrypt secrets</source>
         <translation type="unfinished"></translation>
     </message>
@@ -362,12 +364,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Enter your recovery key or passphrase called %1 to decrypt your secrets:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+17"/>
+        <location line="+22"/>
         <source>Decryption failed</source>
         <translation type="unfinished"></translation>
     </message>
@@ -431,7 +433,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+186"/>
+        <location line="+187"/>
         <source>People</source>
         <translation type="unfinished"></translation>
     </message>
@@ -495,70 +497,68 @@
     </message>
 </context>
 <context>
-    <name>EncryptionIndicator</name>
+    <name>Encrypted</name>
     <message>
-        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
-        <source>This message is not encrypted!</source>
-        <translation>此条信息没有加密</translation>
+        <location filename="../qml/delegates/Encrypted.qml" line="+22"/>
+        <source>There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
-        <source>Encrypted by a verified device</source>
+        <location line="+2"/>
+        <source>This message couldn&apos;t be decrypted, because we only have a key for newer messages. You can try requesting access to this message.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
+        <source>There was an internal error reading the decryption key from the database.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+2"/>
-        <source>Encrypted by an unverified device</source>
+        <source>There was an error decrypting this message.</source>
         <translation type="unfinished"></translation>
     </message>
-</context>
-<context>
-    <name>EventStore</name>
     <message>
-        <location filename="../../src/timeline/EventStore.cpp" line="+663"/>
-        <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="+2"/>
+        <source>The message couldn&apos;t be parsed.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Encrypted Event (Key not valid for this index) --</source>
-        <comment>Placeholder, when the message can&apos;t be decrypted with this key since it is not valid for this index </comment>
+        <location line="+2"/>
+        <source>The encryption key was reused! Someone is possibly trying to insert false messages into this chat!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+35"/>
-        <location line="+63"/>
-        <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="+2"/>
+        <source>Unknown decryption error</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-49"/>
-        <location line="+62"/>
-        <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 as %1.</comment>
+        <location line="+10"/>
+        <source>Request key</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>EncryptionIndicator</name>
+    <message>
+        <location filename="../qml/EncryptionIndicator.qml" line="+34"/>
+        <source>This message is not encrypted!</source>
+        <translation>此条信息没有加密</translation>
+    </message>
     <message>
-        <location line="-52"/>
-        <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>Encrypted by a verified device</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+13"/>
-        <source>-- Replay attack! This message index was reused! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device, but you have trusted that user so far.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
-        <source>-- Message by unverified device! --</source>
+        <location line="+2"/>
+        <source>Encrypted by an unverified device or the key is from an untrusted source like the key backup.</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
@@ -581,17 +581,26 @@
     </message>
     <message>
         <location line="+2"/>
-        <location line="+4"/>
         <source>Device verification timed out.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-2"/>
+        <location line="+2"/>
         <source>Other party canceled the verification.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+18"/>
+        <location line="+2"/>
+        <source>Verification messages received out of order!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Unknown verification error.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+14"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -604,6 +613,81 @@
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ImagePackEditorDialog</name>
+    <message>
+        <location filename="../qml/dialogs/ImagePackEditorDialog.qml" line="+24"/>
+        <source>Editing image pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+63"/>
+        <source>Add images</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>Stickers (*.png *.webp *.gif *.jpg *.jpeg)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+61"/>
+        <source>State key</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Packname</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Attribution</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <location line="+66"/>
+        <source>Use as Emoji</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-56"/>
+        <location line="+66"/>
+        <source>Use as Sticker</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-30"/>
+        <source>Shortcode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Body</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+30"/>
+        <source>Remove from pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <source>Cancel</source>
+        <translation type="unfinished">取消</translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ImagePackSettingsDialog</name>
     <message>
@@ -612,7 +696,17 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+151"/>
+        <location line="+54"/>
+        <source>Create account pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
+        <source>New room pack</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+21"/>
         <source>Private pack</source>
         <translation type="unfinished"></translation>
     </message>
@@ -627,7 +721,7 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+59"/>
+        <location line="+66"/>
         <source>Enable globally</source>
         <translation type="unfinished"></translation>
     </message>
@@ -637,7 +731,12 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+62"/>
+        <location line="+10"/>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+64"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -645,7 +744,7 @@
 <context>
     <name>InputBar</name>
     <message>
-        <location filename="../../src/timeline/InputBar.cpp" line="+234"/>
+        <location filename="../../src/timeline/InputBar.cpp" line="+268"/>
         <source>Select a file</source>
         <translation type="unfinished">选择一个文件</translation>
     </message>
@@ -655,7 +754,7 @@
         <translation type="unfinished">所有文件(*)</translation>
     </message>
     <message>
-        <location line="+442"/>
+        <location line="+474"/>
         <source>Failed to upload media. Please try again.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -663,7 +762,7 @@
 <context>
     <name>InviteDialog</name>
     <message>
-        <location filename="../qml/InviteDialog.qml" line="+32"/>
+        <location filename="../qml/dialogs/InviteDialog.qml" line="+33"/>
         <source>Invite users to %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -694,6 +793,32 @@
         <translation type="unfinished">取消</translation>
     </message>
 </context>
+<context>
+    <name>JoinRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/JoinRoomDialog.qml" line="+14"/>
+        <source>Join room</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Room ID or alias</source>
+        <translation type="unfinished">聊天室 ID 或别名</translation>
+    </message>
+</context>
+<context>
+    <name>LeaveRoomDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LeaveRoomDialog.qml" line="+15"/>
+        <source>Leave room</source>
+        <translation type="unfinished">离开聊天室</translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Are you sure you want to leave?</source>
+        <translation type="unfinished">你确定要离开吗?</translation>
+    </message>
+</context>
 <context>
     <name>LoginPage</name>
     <message>
@@ -756,25 +881,25 @@ Example: https://server.my:8787</source>
         <translation>登录</translation>
     </message>
     <message>
-        <location line="+84"/>
+        <location line="+83"/>
         <location line="+11"/>
-        <location line="+157"/>
+        <location line="+151"/>
         <location line="+11"/>
         <source>You have entered an invalid Matrix ID  e.g @joe:matrix.org</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-131"/>
+        <location line="-126"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+24"/>
+        <location line="+22"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation>没找到要求的终端。可能不是一个 Matrix 服务器。</translation>
     </message>
@@ -784,30 +909,48 @@ Example: https://server.my:8787</source>
         <translation>收到形式错误的响应。请确认服务器域名合法。</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>An unknown error occured. Make sure the homeserver domain is valid.</source>
         <translation>发生了一个未知错误。请确认服务器域名合法。</translation>
     </message>
     <message>
-        <location line="-168"/>
+        <location line="-164"/>
         <source>SSO LOGIN</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+264"/>
+        <location line="+257"/>
         <source>Empty password</source>
         <translation>空密码</translation>
     </message>
     <message>
-        <location line="+57"/>
+        <location line="+55"/>
         <source>SSO login failed</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>LogoutDialog</name>
+    <message>
+        <location filename="../qml/dialogs/LogoutDialog.qml" line="+13"/>
+        <source>Log out</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>A call is in progress. Log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Are you sure you want to log out?</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>MessageDelegate</name>
     <message>
-        <location filename="../qml/delegates/MessageDelegate.qml" line="+169"/>
+        <location filename="../qml/delegates/MessageDelegate.qml" line="+174"/>
         <location line="+9"/>
         <source>removed</source>
         <translation type="unfinished"></translation>
@@ -818,7 +961,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+22"/>
         <source>room name changed to: %1</source>
         <translation type="unfinished"></translation>
     </message>
@@ -877,6 +1020,11 @@ Example: https://server.my:8787</source>
         <source>Negotiating call...</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+70"/>
+        <source>Allow them in</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>MessageInput</name>
@@ -901,7 +1049,7 @@ Example: https://server.my:8787</source>
         <translation>写一条消息…</translation>
     </message>
     <message>
-        <location line="+214"/>
+        <location line="+234"/>
         <source>Stickers</source>
         <translation type="unfinished"></translation>
     </message>
@@ -924,7 +1072,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>MessageView</name>
     <message>
-        <location filename="../qml/MessageView.qml" line="+87"/>
+        <location filename="../qml/MessageView.qml" line="+88"/>
         <source>Edit</source>
         <translation type="unfinished"></translation>
     </message>
@@ -944,17 +1092,19 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+405"/>
+        <location line="+420"/>
+        <location line="+118"/>
         <source>&amp;Copy</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="-111"/>
+        <location line="+118"/>
         <source>Copy &amp;link location</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+8"/>
+        <location line="-110"/>
         <source>Re&amp;act</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1013,6 +1163,11 @@ Example: https://server.my:8787</source>
         <source>Copy link to eve&amp;nt</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+43"/>
+        <source>&amp;Go to quoted message</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>NewVerificationRequest</name>
@@ -1027,7 +1182,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+16"/>
+        <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1073,32 +1233,28 @@ Example: https://server.my:8787</source>
     </message>
 </context>
 <context>
-    <name>NotificationsManager</name>
+    <name>NotificationWarning</name>
     <message>
-        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
-        <location filename="../../src/notifications/ManagerMac.cpp" line="+44"/>
-        <location filename="../../src/notifications/ManagerWin.cpp" line="+78"/>
-        <source>%1 sent an encrypted message</source>
+        <location filename="../qml/NotificationWarning.qml" line="+32"/>
+        <source>You are about to notify the whole room</source>
         <translation type="unfinished"></translation>
     </message>
+</context>
+<context>
+    <name>NotificationsManager</name>
     <message>
-        <location line="+4"/>
-        <source>* %1 %2</source>
-        <comment>Format an emote message in a notification, %1 is the sender, %2 the message</comment>
+        <location filename="../../src/notifications/Manager.cpp" line="+22"/>
+        <location filename="../../src/notifications/ManagerMac.cpp" line="+45"/>
+        <location filename="../../src/notifications/ManagerWin.cpp" line="+74"/>
+        <source>%1 sent an encrypted message</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+6"/>
         <source>%1 replied: %2</source>
         <comment>Format a reply in a notification. %1 is the sender, %2 the message</comment>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+4"/>
-        <source>%1: %2</source>
-        <comment>Format a normal message in a notification. %1 is the sender, %2 the message</comment>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-1"/>
         <location filename="../../src/notifications/ManagerWin.cpp" line="-1"/>
@@ -1129,7 +1285,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+22"/>
+        <location line="+23"/>
         <source>Voice</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1160,7 +1316,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>QCoreApplication</name>
     <message>
-        <location filename="../../src/main.cpp" line="+200"/>
+        <location filename="../../src/main.cpp" line="+191"/>
         <source>Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1175,21 +1331,37 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>ReadReceipts</name>
+    <message>
+        <location filename="../qml/dialogs/ReadReceipts.qml" line="+41"/>
+        <source>Read receipts</source>
+        <translation type="unfinished">阅读回执</translation>
+    </message>
+</context>
+<context>
+    <name>ReadReceiptsModel</name>
+    <message>
+        <location filename="../../src/ReadReceiptsModel.cpp" line="+110"/>
+        <source>Yesterday, %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RegisterPage</name>
     <message>
-        <location filename="../../src/RegisterPage.cpp" line="+78"/>
+        <location filename="../../src/RegisterPage.cpp" line="+81"/>
         <source>Username</source>
         <translation>用户名</translation>
     </message>
     <message>
         <location line="+2"/>
-        <location line="+305"/>
+        <location line="+147"/>
         <source>The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-301"/>
+        <location line="-143"/>
         <source>Password</source>
         <translation>密码</translation>
     </message>
@@ -1209,7 +1381,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+3"/>
         <source>A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1219,27 +1391,17 @@ Example: https://server.my:8787</source>
         <translation>注册</translation>
     </message>
     <message>
-        <location line="+73"/>
-        <source>No supported registration flows!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+213"/>
-        <source>One or more fields have invalid inputs. Please correct those issues and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+23"/>
+        <location line="+169"/>
         <source>Autodiscovery failed. Received malformed response.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+5"/>
         <source>Autodiscovery failed. Unknown error when requesting .well-known.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+26"/>
+        <location line="+24"/>
         <source>The required endpoints were not found. Possibly not a Matrix server.</source>
         <translation type="unfinished">没找到要求的终端。可能不是一个 Matrix 服务器。</translation>
     </message>
@@ -1254,17 +1416,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">发生了一个未知错误。请确认服务器域名合法。</translation>
     </message>
     <message>
-        <location line="-94"/>
+        <location line="-107"/>
         <source>Password is not long enough (min 8 chars)</source>
         <translation>密码不够长(至少8个字符)</translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+11"/>
         <source>Passwords don&apos;t match</source>
         <translation>密码不匹配</translation>
     </message>
     <message>
-        <location line="+6"/>
+        <location line="+11"/>
         <source>Invalid server name</source>
         <translation>无效的服务器名</translation>
     </message>
@@ -1272,7 +1434,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>ReplyPopup</name>
     <message>
-        <location filename="../qml/ReplyPopup.qml" line="+62"/>
+        <location filename="../qml/ReplyPopup.qml" line="+63"/>
         <source>Close</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1282,10 +1444,28 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>RoomDirectory</name>
+    <message>
+        <location filename="../qml/dialogs/RoomDirectory.qml" line="+25"/>
+        <source>Explore Public Rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+168"/>
+        <source>Search for public rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
+        <source>Choose custom homeserver</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>RoomInfo</name>
     <message>
-        <location filename="../../src/Cache.cpp" line="+4180"/>
+        <location filename="../../src/Cache.cpp" line="+4491"/>
         <source>no version stored</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1293,7 +1473,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomList</name>
     <message>
-        <location filename="../qml/RoomList.qml" line="+57"/>
+        <location filename="../qml/RoomList.qml" line="+67"/>
         <source>New tag</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1302,16 +1482,6 @@ Example: https://server.my:8787</source>
         <source>Enter the tag you want to use:</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location line="+9"/>
-        <source>Leave Room</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+1"/>
-        <source>Are you sure you want to leave this room?</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location line="+7"/>
         <source>Leave room</source>
@@ -1343,7 +1513,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+268"/>
+        <location line="+278"/>
         <source>Status Message</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1363,12 +1533,35 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+79"/>
+        <location line="+80"/>
         <source>Logout</source>
         <translation type="unfinished">登出</translation>
     </message>
     <message>
-        <location line="+46"/>
+        <location line="+40"/>
+        <source>Encryption not set up</source>
+        <extracomment>Cross-signing setup has not run yet.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Unverified login</source>
+        <extracomment>The user just signed in with this device and hasn&apos;t verified their master key.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Please verify your other devices</source>
+        <extracomment>There are unverified devices signed in to this account.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+20"/>
+        <source>Close</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+65"/>
         <source>Start a new chat</source>
         <translation type="unfinished">开始新的聊天</translation>
     </message>
@@ -1388,7 +1581,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">聊天室目录</translation>
     </message>
     <message>
-        <location line="+12"/>
+        <location line="+16"/>
         <source>User settings</source>
         <translation type="unfinished">用户设置</translation>
     </message>
@@ -1396,12 +1589,12 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomMembers</name>
     <message>
-        <location filename="../qml/RoomMembers.qml" line="+17"/>
+        <location filename="../qml/dialogs/RoomMembers.qml" line="+19"/>
         <source>Members of %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+32"/>
+        <location line="+33"/>
         <source>%n people in %1</source>
         <comment>Summary above list of members</comment>
         <translation type="unfinished">
@@ -1413,16 +1606,36 @@ Example: https://server.my:8787</source>
         <source>Invite more people</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+76"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This user is verified.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user isn&apos;t verified, but is still using the same master key from the first time you met.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This user has unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>RoomSettings</name>
     <message>
-        <location filename="../qml/RoomSettings.qml" line="+26"/>
+        <location filename="../qml/dialogs/RoomSettings.qml" line="+26"/>
         <source>Room Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+80"/>
+        <location line="+81"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1452,7 +1665,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+15"/>
+        <location line="+9"/>
+        <source>Room access</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>Anyone and guests</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1467,7 +1685,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+2"/>
+        <source>By knocking</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
+        <source>Restricted by membership in other rooms</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+12"/>
         <source>Encryption</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1513,12 +1741,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/RoomSettings.cpp" line="+255"/>
+        <location filename="../../src/ui/RoomSettings.cpp" line="+254"/>
         <source>Failed to enable encryption: %1</source>
         <translation type="unfinished">启用加密失败:%1</translation>
     </message>
     <message>
-        <location line="+228"/>
+        <location line="+249"/>
         <source>Select an avatar</source>
         <translation type="unfinished">选择一个头像</translation>
     </message>
@@ -1538,8 +1766,8 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
-        <location line="+20"/>
+        <location line="+32"/>
+        <location line="+19"/>
         <source>Failed to upload image: %s</source>
         <translation type="unfinished">上传图像失败:%s</translation>
     </message>
@@ -1547,21 +1775,49 @@ Example: https://server.my:8787</source>
 <context>
     <name>RoomlistModel</name>
     <message>
-        <location filename="../../src/timeline/RoomlistModel.cpp" line="+143"/>
+        <location filename="../../src/timeline/RoomlistModel.cpp" line="+147"/>
         <source>Pending invite.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+30"/>
+        <location line="+35"/>
         <source>Previewing this room</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+34"/>
+        <location line="+38"/>
         <source>No preview available</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
+<context>
+    <name>Root</name>
+    <message>
+        <location filename="../qml/Root.qml" line="+255"/>
+        <source>Please enter your login password to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid email address to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter a valid phone number to continue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Please enter the token, which has been sent to you:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Wait for the confirmation link to arrive, then continue.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ScreenShare</name>
     <message>
@@ -1616,6 +1872,121 @@ Example: https://server.my:8787</source>
         <translation type="unfinished">取消</translation>
     </message>
 </context>
+<context>
+    <name>SecretStorage</name>
+    <message>
+        <location filename="../../src/Cache.cpp" line="-3725"/>
+        <source>Failed to connect to secret storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationCheck</name>
+    <message>
+        <location filename="../qml/SelfVerificationCheck.qml" line="+39"/>
+        <source>This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don&apos;t share it with anyone and don&apos;t lose it! Do not pass go! Do not collect $200!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+33"/>
+        <source>Encryption setup successfully</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Failed to setup encryption: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Setup Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>Hello and welcome to Matrix!
+It seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+124"/>
+        <source>Activate Encryption</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.
+If you choose verify, you need to have the other device available. If you choose &quot;enter passphrase&quot;, you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>enter passphrase</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SelfVerificationStatus</name>
+    <message>
+        <location filename="../../src/encryption/SelfVerificationStatus.cpp" line="+40"/>
+        <source>Failed to create keys for cross-signing!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+16"/>
+        <source>Failed to create keys for online key backup!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to create keys secure server side secret storage!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+44"/>
+        <source>Encryption Setup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+6"/>
+        <source>Encryption setup failed: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SingleImagePackModel</name>
+    <message>
+        <location filename="../../src/SingleImagePackModel.cpp" line="+255"/>
+        <location line="+25"/>
+        <source>Failed to update image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-12"/>
+        <source>Failed to delete old image pack: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
+        <source>Failed to open image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Failed to upload image: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>StatusIndicator</name>
     <message>
@@ -1668,18 +2039,18 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineModel</name>
     <message>
-        <location filename="../../src/timeline/TimelineModel.cpp" line="+1107"/>
+        <location filename="../../src/timeline/TimelineModel.cpp" line="+1104"/>
         <source>Message redaction failed: %1</source>
         <translation type="unfinished">删除消息失败:%1</translation>
     </message>
     <message>
-        <location line="+73"/>
+        <location line="+71"/>
         <location line="+5"/>
         <source>Failed to encrypt event, sending aborted!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+173"/>
+        <location line="+169"/>
         <source>Save image</source>
         <translation type="unfinished">保存图像</translation>
     </message>
@@ -1699,7 +2070,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location line="+228"/>
+        <location line="+251"/>
         <source>%1 and %2 are typing.</source>
         <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment>
         <translation type="unfinished">
@@ -1707,7 +2078,7 @@ Example: https://server.my:8787</source>
         </translation>
     </message>
     <message>
-        <location line="+68"/>
+        <location line="+66"/>
         <source>%1 opened the room to the public.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1717,7 +2088,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+23"/>
+        <location line="+2"/>
+        <source>%1 allowed to join this room by knocking.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
+        <source>%1 allowed members of the following rooms to automatically join this room: %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>%1 made the room open to guests.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1737,12 +2118,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they were invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 set the room history visible to members since they joined the room.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1752,12 +2133,12 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+33"/>
+        <location line="+76"/>
         <source>%1 was invited.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+19"/>
+        <location line="+18"/>
         <source>%1 changed their avatar.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1767,12 +2148,17 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+4"/>
+        <location line="+5"/>
         <source>%1 joined.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+3"/>
+        <source>%1 joined via authorisation from %2&apos;s server.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+11"/>
         <source>%1 rejected their invite.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1802,32 +2188,32 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+9"/>
+        <location line="+8"/>
         <source>Reason: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-20"/>
+        <location line="-19"/>
         <source>%1 redacted their knock.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-884"/>
+        <location line="-951"/>
         <source>You joined this room.</source>
         <translation type="unfinished">您已加入此房间</translation>
     </message>
     <message>
-        <location line="+850"/>
+        <location line="+912"/>
         <source>%1 has changed their avatar and changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>%1 has changed their display name to %2.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+31"/>
+        <location line="+37"/>
         <source>Rejected the knock from %1.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1846,7 +2232,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineRow</name>
     <message>
-        <location filename="../qml/TimelineRow.qml" line="+180"/>
+        <location filename="../qml/TimelineRow.qml" line="+183"/>
         <source>Edited</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1854,12 +2240,17 @@ Example: https://server.my:8787</source>
 <context>
     <name>TimelineView</name>
     <message>
-        <location filename="../qml/TimelineView.qml" line="+30"/>
+        <location filename="../qml/TimelineView.qml" line="+29"/>
         <source>No room open</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+139"/>
+        <location line="+137"/>
+        <source>No preview available</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+7"/>
         <source>%1 member(s)</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1884,28 +2275,40 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>TimelineViewManager</name>
-    <message>
-        <location filename="../../src/timeline/TimelineViewManager.cpp" line="+527"/>
-        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>TopBar</name>
     <message>
-        <location filename="../qml/TopBar.qml" line="+54"/>
+        <location filename="../qml/TopBar.qml" line="+59"/>
         <source>Back to room list</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-39"/>
+        <location line="-44"/>
         <source>No room selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+90"/>
+        <location line="+96"/>
+        <source>This room is not encrypted!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+4"/>
+        <source>This room contains only verified devices.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains verified devices and devices which have never changed their master key.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>This room contains unverified devices!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+15"/>
         <source>Room options</source>
         <translation type="unfinished">聊天室选项</translation>
     </message>
@@ -1943,10 +2346,35 @@ Example: https://server.my:8787</source>
         <translation>退出</translation>
     </message>
 </context>
+<context>
+    <name>UIA</name>
+    <message>
+        <location filename="../../src/ui/UIA.cpp" line="+59"/>
+        <source>No available registration flows!</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+26"/>
+        <location line="+24"/>
+        <location line="+17"/>
+        <source>Registration aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-27"/>
+        <source>Please enter a valid registration token.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+165"/>
+        <source>Invalid token</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>UserProfile</name>
     <message>
-        <location filename="../qml/UserProfile.qml" line="+25"/>
+        <location filename="../qml/dialogs/UserProfile.qml" line="+28"/>
         <source>Global User Profile</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1956,33 +2384,98 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+115"/>
-        <location line="+107"/>
-        <source>Verify</source>
+        <location line="+49"/>
+        <source>Change avatar globally.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-54"/>
-        <source>Ban the user</source>
+        <location line="+0"/>
+        <source>Change avatar. Will only apply to this room.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-17"/>
-        <source>Start a private chat</source>
+        <location line="+80"/>
+        <source>Change display name globally.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+0"/>
+        <source>Change display name. Will only apply to this room.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+29"/>
+        <source>Room: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>This is a room-specific profile. The user&apos;s name and avatar may be different from their global versions.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+13"/>
+        <source>Open the global profile for this user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <location line="+197"/>
+        <source>Verify</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="-160"/>
+        <source>Start a private chat.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
         <location line="+8"/>
-        <source>Kick the user</source>
+        <source>Kick the user.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+63"/>
+        <location line="+9"/>
+        <source>Ban the user.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+9"/>
+        <source>Refresh device list.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+54"/>
+        <source>Sign out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+31"/>
+        <source>Change device name.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+22"/>
+        <source>Last seen %1 from %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+27"/>
         <source>Unverify</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../../src/ui/UserProfile.cpp" line="+307"/>
+        <location filename="../../src/ui/UserProfile.cpp" line="+152"/>
+        <source>Sign out device %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+10"/>
+        <source>You signed out this device.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+223"/>
         <source>Select an avatar</source>
         <translation type="unfinished">选择一个头像</translation>
     </message>
@@ -2005,8 +2498,8 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettings</name>
     <message>
-        <location filename="../../src/UserSettingsPage.cpp" line="+363"/>
-        <location filename="../../src/UserSettingsPage.h" line="+194"/>
+        <location filename="../../src/UserSettingsPage.cpp" line="+374"/>
+        <location filename="../../src/UserSettingsPage.h" line="+204"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2014,7 +2507,7 @@ Example: https://server.my:8787</source>
 <context>
     <name>UserSettingsPage</name>
     <message>
-        <location line="+525"/>
+        <location line="+567"/>
         <source>Minimize to tray</source>
         <translation>最小化至托盘</translation>
     </message>
@@ -2024,22 +2517,22 @@ Example: https://server.my:8787</source>
         <translation>在托盘启动</translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+10"/>
         <source>Group&apos;s sidebar</source>
         <translation>群组侧边栏</translation>
     </message>
     <message>
-        <location line="-3"/>
+        <location line="-6"/>
         <source>Circular Avatars</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-210"/>
+        <location line="-217"/>
         <source>profile: %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+96"/>
+        <location line="+104"/>
         <source>Default</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2064,7 +2557,7 @@ Example: https://server.my:8787</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+27"/>
+        <location line="+26"/>
         <source>Keep the application running in the background after closing the client window.</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2079,6 +2572,16 @@ Example: https://server.my:8787</source>
 OFF - square, ON - Circle.</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location line="+1"/>
+        <source>Use identicons</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Display an identicon instead of a letter when a user has not set an avatar.</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location line="+3"/>
         <source>Show a column containing groups and tags next to the room list.</source>
@@ -2107,7 +2610,7 @@ be blurred.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Privacy screen timeout (in seconds [0 - 3600])</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2162,7 +2665,7 @@ If this is on, rooms which have active notifications (the small circle with a nu
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+7"/>
+        <location line="+6"/>
         <source>Read receipts</source>
         <translation>阅读回执</translation>
     </message>
@@ -2173,7 +2676,7 @@ Status is displayed next to timestamps.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+2"/>
+        <location line="+1"/>
         <source>Send messages as Markdown</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2185,6 +2688,11 @@ When disabled, all messages are sent as a plain text.</source>
     </message>
     <message>
         <location line="+2"/>
+        <source>Play animated images only on hover</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+3"/>
         <source>Desktop notifications</source>
         <translation>桌面通知</translation>
     </message>
@@ -2225,12 +2733,47 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+56"/>
+        <location line="+55"/>
+        <source>Send encrypted messages to verified users only</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
         <source>Share keys with verified users and devices</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+406"/>
+        <location line="+2"/>
+        <source>Automatically replies to key requests from other users, if they are verified, even if that device shouldn&apos;t have access to those keys otherwise.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Online Key Backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+2"/>
+        <source>Download message encryption keys from and upload to the encrypted online key backup.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+178"/>
+        <source>Enable online key backup</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+1"/>
+        <source>The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+253"/>
         <source>CACHED</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2240,7 +2783,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="-460"/>
+        <location line="-495"/>
         <source>Scale factor</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2315,7 +2858,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>设备指纹</translation>
     </message>
     <message>
-        <location line="-164"/>
+        <location line="-166"/>
         <source>Session Keys</source>
         <translation>会话密钥</translation>
     </message>
@@ -2335,17 +2878,22 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>加密</translation>
     </message>
     <message>
-        <location line="-115"/>
+        <location line="-123"/>
         <source>GENERAL</source>
         <translation>通用</translation>
     </message>
     <message>
-        <location line="+64"/>
+        <location line="+72"/>
         <source>INTERFACE</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+194"/>
+        <location line="+179"/>
+        <source>Plays media like GIFs or WEBPs only when explicitly hovering over them.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location line="+17"/>
         <source>Touchscreen mode</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2360,12 +2908,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+40"/>
-        <source>Automatically replies to key requests from other users, if they are verified.</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+5"/>
+        <location line="+53"/>
         <source>Master signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2385,7 +2928,7 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>Self signing key</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2415,14 +2958,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished">所有文件(*)</translation>
     </message>
     <message>
-        <location line="+236"/>
+        <location line="+265"/>
         <source>Open Sessions File</source>
         <translation>打开会话文件</translation>
     </message>
     <message>
         <location line="+4"/>
         <location line="+18"/>
-        <location line="+9"/>
+        <location line="+8"/>
         <location line="+19"/>
         <location line="+11"/>
         <location line="+18"/>
@@ -2430,19 +2973,19 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>错误</translation>
     </message>
     <message>
-        <location line="-66"/>
-        <location line="+28"/>
+        <location line="-65"/>
+        <location line="+27"/>
         <source>File Password</source>
         <translation>文件密码</translation>
     </message>
     <message>
-        <location line="-27"/>
+        <location line="-26"/>
         <source>Enter the passphrase to decrypt the file:</source>
         <translation>输入密码以解密文件:</translation>
     </message>
     <message>
         <location line="+8"/>
-        <location line="+28"/>
+        <location line="+27"/>
         <source>The password cannot be empty</source>
         <translation>密码不能为空</translation>
     </message>
@@ -2457,6 +3000,14 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation>保存导出的会话密钥的文件</translation>
     </message>
 </context>
+<context>
+    <name>VerificationManager</name>
+    <message>
+        <location filename="../../src/encryption/VerificationManager.cpp" line="+105"/>
+        <source>No encrypted private chat found with this user. Create an encrypted private chat with this user and try again.</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>Waiting</name>
     <message>
@@ -2582,37 +3133,6 @@ This usually causes the application icon in the task bar to animate in some fash
         <translation type="unfinished"></translation>
     </message>
 </context>
-<context>
-    <name>dialogs::JoinRoom</name>
-    <message>
-        <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/>
-        <source>Join</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+2"/>
-        <source>Cancel</source>
-        <translation>取消</translation>
-    </message>
-    <message>
-        <location line="+7"/>
-        <source>Room ID or alias</source>
-        <translation>聊天室 ID 或别名</translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::LeaveRoom</name>
-    <message>
-        <location filename="../../src/dialogs/LeaveRoom.cpp" line="+35"/>
-        <source>Cancel</source>
-        <translation>取消</translation>
-    </message>
-    <message>
-        <location line="+8"/>
-        <source>Are you sure you want to leave?</source>
-        <translation>你确定要离开吗?</translation>
-    </message>
-</context>
 <context>
     <name>dialogs::Logout</name>
     <message>
@@ -2666,32 +3186,6 @@ Media size: %2
         <translation>解决 reCAPTCHA 并按确认按钮</translation>
     </message>
 </context>
-<context>
-    <name>dialogs::ReadReceipts</name>
-    <message>
-        <location filename="../../src/dialogs/ReadReceipts.cpp" line="+124"/>
-        <source>Read receipts</source>
-        <translation>阅读回执</translation>
-    </message>
-    <message>
-        <location line="+4"/>
-        <source>Close</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
-<context>
-    <name>dialogs::ReceiptItem</name>
-    <message>
-        <location line="-46"/>
-        <source>Today %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location line="+3"/>
-        <source>Yesterday %1</source>
-        <translation type="unfinished"></translation>
-    </message>
-</context>
 <context>
     <name>message-description sent:</name>
     <message>
@@ -2705,47 +3199,47 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent an image</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a file</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a video</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 sent a sticker</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You sent a notification</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2760,7 +3254,7 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1: %2</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2780,27 +3274,27 @@ Media size: %2
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 placed a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 answered a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+5"/>
+        <location line="+4"/>
         <source>You ended a call</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location line="+3"/>
+        <location line="+2"/>
         <source>%1 ended a call</source>
         <translation type="unfinished"></translation>
     </message>
@@ -2808,7 +3302,7 @@ Media size: %2
 <context>
     <name>utils</name>
     <message>
-        <location line="+4"/>
+        <location line="+3"/>
         <source>Unknown Message Type</source>
         <translation type="unfinished"></translation>
     </message>
diff --git a/resources/nheko.appdata.xml b/resources/nheko.appdata.xml
index 8cd2bfa3caaab24e43b42bee1651e33b9cbe8071..4fe9e722c5b3b4d1a184b263991f9dbd309cd9b5 100644
--- a/resources/nheko.appdata.xml
+++ b/resources/nheko.appdata.xml
@@ -3,7 +3,7 @@
 <component type="desktop">
   <id>nheko.desktop</id>
   <metadata_license>CC0-1.0</metadata_license>
-  <project_license>GPL-3.0-or-later and CC-BY</project_license>
+  <project_license>GPL-3.0-or-later</project_license>
   <name>nheko</name>
   <summary>Desktop client for the Matrix protocol</summary>
   <description>
diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 4a9a565cd71db5ed2de8293f633efd8a8f9de006..58b22863f864f8563fa32c6f481483f6a9050b50 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -12,6 +12,7 @@ Rectangle {
 
     property string url
     property string userid
+    property string roomid
     property string displayName
     property alias textColor: label.color
     property bool crop: true
@@ -35,10 +36,29 @@ Rectangle {
         font.pixelSize: avatar.height / 2
         verticalAlignment: Text.AlignVCenter
         horizontalAlignment: Text.AlignHCenter
-        visible: img.status != Image.Ready
+        visible: img.status != Image.Ready && !Settings.useIdenticon
         color: Nheko.colors.text
     }
 
+    Image {
+        id: identicon
+
+        anchors.fill: parent
+        visible: Settings.useIdenticon && img.status != Image.Ready
+        source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : ""
+
+        MouseArea {
+            anchors.fill: parent
+
+            Ripple {
+                rippleTarget: parent
+                color: Qt.rgba(Nheko.colors.alternateBase.r, Nheko.colors.alternateBase.g, Nheko.colors.alternateBase.b, 0.5)
+            }
+
+        }
+
+    }
+
     Image {
         id: img
 
@@ -49,7 +69,7 @@ Rectangle {
         smooth: true
         sourceSize.width: avatar.width
         sourceSize.height: avatar.height
-        source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100.0 : 25.0) + ((avatar.crop) ? "" : "&scale")) : ""
+        source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale")) : ""
 
         MouseArea {
             id: mouseArea
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index e56d7d46dcb3ad44ac23469d0dffb92ef12fc94d..22a04b740d14bd4f3c32c0606d22163b6c3bf978 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -2,12 +2,15 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import QtQuick 2.9
-import QtQuick.Controls 2.5
+import QtQuick 2.15
+import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 import "components"
 import im.nheko 1.0
 
+// this needs to be last
+import QtQml 2.15
+
 Rectangle {
     id: chatPage
 
@@ -18,7 +21,7 @@ Rectangle {
 
         anchors.fill: parent
         singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width
-        pageIndex: Rooms.currentRoom ? 2 : 1
+        pageIndex: (Rooms.currentRoom || Rooms.currentRoomPreview.roomid) ? 2 : 1
 
         AdaptiveLayoutElement {
             id: communityListC
@@ -41,6 +44,7 @@ Rectangle {
                 value: communityListC.preferredWidth
                 when: !adaptiveView.singlePageMode
                 delayed: true
+                restoreMode: Binding.RestoreBindingOrValue
             }
 
         }
@@ -66,6 +70,7 @@ Rectangle {
                 value: roomListC.preferredWidth
                 when: !adaptiveView.singlePageMode
                 delayed: true
+                restoreMode: Binding.RestoreBindingOrValue
             }
 
         }
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 491913bef50969eb2b1c7ee2d787582bf9434722..6a2c642c689803dd8839dd393d243761e7de28da 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -47,29 +47,32 @@ Page {
 
         }
 
-        delegate: Rectangle {
+        delegate: ItemDelegate {
             id: communityItem
 
-            property color background: Nheko.colors.window
+            property color backgroundColor: Nheko.colors.window
             property color importantText: Nheko.colors.text
             property color unimportantText: Nheko.colors.buttonText
             property color bubbleBackground: Nheko.colors.highlight
             property color bubbleText: Nheko.colors.highlightedText
 
-            color: background
+            background: Rectangle {
+                color: backgroundColor
+            }
+
             height: avatarSize + 2 * Nheko.paddingMedium
             width: ListView.view.width
             state: "normal"
-            ToolTip.visible: hovered.hovered && collapsed
+            ToolTip.visible: hovered && collapsed
             ToolTip.text: model.tooltip
             states: [
                 State {
                     name: "highlight"
-                    when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id)
+                    when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId == model.id)
 
                     PropertyChanges {
                         target: communityItem
-                        background: Nheko.colors.dark
+                        backgroundColor: Nheko.colors.dark
                         importantText: Nheko.colors.brightText
                         unimportantText: Nheko.colors.brightText
                         bubbleBackground: Nheko.colors.highlight
@@ -83,7 +86,7 @@ Page {
 
                     PropertyChanges {
                         target: communityItem
-                        background: Nheko.colors.highlight
+                        backgroundColor: Nheko.colors.highlight
                         importantText: Nheko.colors.highlightedText
                         unimportantText: Nheko.colors.highlightedText
                         bubbleBackground: Nheko.colors.highlightedText
@@ -93,25 +96,21 @@ Page {
                 }
             ]
 
-            TapHandler {
-                margin: -Nheko.paddingSmall
-                acceptedButtons: Qt.RightButton
-                onSingleTapped: communityContextMenu.show(model.id)
-                gesturePolicy: TapHandler.ReleaseWithinBounds
-            }
-
-            TapHandler {
-                margin: -Nheko.paddingSmall
-                onSingleTapped: Communities.setCurrentTagId(model.id)
-                onLongPressed: communityContextMenu.show(model.id)
-            }
+            Item {
+                anchors.fill: parent
 
-            HoverHandler {
-                id: hovered
+                TapHandler {
+                    acceptedButtons: Qt.RightButton
+                    onSingleTapped: communityContextMenu.show(model.id)
+                    gesturePolicy: TapHandler.ReleaseWithinBounds
+                    acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+                }
 
-                margin: -Nheko.paddingSmall
             }
 
+            onClicked: Communities.setCurrentTagId(model.id)
+            onPressAndHold: communityContextMenu.show(model.id)
+
             RowLayout {
                 spacing: Nheko.paddingMedium
                 anchors.fill: parent
@@ -130,8 +129,9 @@ Page {
                         else
                             return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
                     }
+                    roomid: model.id
                     displayName: model.displayName
-                    color: communityItem.background
+                    color: communityItem.backgroundColor
                 }
 
                 ElidedLabel {
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 00fc3216131e088bc227f8917112126eb059e649..6bde67fa11af405ed40e243ed91e03a0035c67e9 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -139,6 +139,7 @@ Popup {
                             height: popup.avatarHeight
                             width: popup.avatarWidth
                             displayName: model.displayName
+                            userid: model.userid
                             url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
                             onClicked: popup.completionClicked(completer.completionAt(model.index))
                         }
@@ -194,6 +195,7 @@ Popup {
                             height: popup.avatarHeight
                             width: popup.avatarWidth
                             displayName: model.roomName
+                            roomid: model.roomid
                             url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
                             onClicked: {
                                 popup.completionClicked(completer.completionAt(model.index));
@@ -225,6 +227,7 @@ Popup {
                             height: popup.avatarHeight
                             width: popup.avatarWidth
                             displayName: model.roomName
+                            roomid: model.roomid
                             url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
                             onClicked: popup.completionClicked(completer.completionAt(model.index))
                         }
diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml
index 52d2eeed723f12c6472d4271355d983eab4ad8c1..6bc16a18c83a0ed6373ea4b614e63e1527987445 100644
--- a/resources/qml/EncryptionIndicator.qml
+++ b/resources/qml/EncryptionIndicator.qml
@@ -39,7 +39,7 @@ Image {
         case Crypto.TOFU:
             return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
         default:
-            return qsTr("Encrypted by an unverified device");
+            return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup.");
         }
     }
 
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 26752f92c9bdc60fde1212cd1ececc956174dd27..eccd6ce938ec2e2b8de6318a02ee78867a57c4dc 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -80,15 +80,15 @@ Popup {
                 completerPopup.completer.searchString = text;
             }
             Keys.onPressed: {
-                if (event.key == Qt.Key_Up && completerPopup.opened) {
+                if ((event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) && completerPopup.opened) {
                     event.accepted = true;
                     completerPopup.up();
-                } else if (event.key == Qt.Key_Down && completerPopup.opened) {
+                } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
                     event.accepted = true;
-                    completerPopup.down();
-                } else if (event.key == Qt.Key_Tab && completerPopup.opened) {
-                    event.accepted = true;
-                    completerPopup.down();
+                    if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
+                        completerPopup.up();
+                    else
+                        completerPopup.down();
                 } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                     completerPopup.finishCompletion();
                     event.accepted = true;
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 7fb0968419b576708919cd671d0720f230010a8a..c95929ce3960ea329ee4870a7c980eb1336aeca3 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -93,7 +93,7 @@ Rectangle {
             TextArea {
                 id: messageInput
 
-                property int completerTriggeredAt: -1
+                property int completerTriggeredAt: 0
 
                 function insertCompletion(completion) {
                     messageInput.remove(completerTriggeredAt, cursorPosition);
@@ -134,10 +134,9 @@ Rectangle {
                         return ;
 
                     room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
-                    if (cursorPosition <= completerTriggeredAt) {
-                        completerTriggeredAt = -1;
+                    if (popup.opened && cursorPosition <= completerTriggeredAt)
                         popup.close();
-                    }
+
                     if (popup.opened)
                         popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
 
@@ -145,7 +144,7 @@ Rectangle {
                 onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
                 onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
                 // Ensure that we get escape key press events first.
-                Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter))
+                Keys.onShortcutOverride: event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter))
                 Keys.onPressed: {
                     if (event.matches(StandardKey.Paste)) {
                         room.input.paste(false);
@@ -165,18 +164,20 @@ Rectangle {
                     } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
                         messageInput.text = room.input.nextText();
                     } else if (event.key == Qt.Key_At) {
-                        messageInput.openCompleter(cursorPosition, "user");
+                        messageInput.openCompleter(selectionStart, "user");
                         popup.open();
                     } else if (event.key == Qt.Key_Colon) {
-                        messageInput.openCompleter(cursorPosition, "emoji");
+                        messageInput.openCompleter(selectionStart, "emoji");
                         popup.open();
                     } else if (event.key == Qt.Key_NumberSign) {
-                        messageInput.openCompleter(cursorPosition, "roomAliases");
+                        messageInput.openCompleter(selectionStart, "roomAliases");
                         popup.open();
                     } else if (event.key == Qt.Key_Escape && popup.opened) {
-                        completerTriggeredAt = -1;
                         popup.completerName = "";
+                        popup.close();
                         event.accepted = true;
+                    } else if (event.matches(StandardKey.SelectAll) && popup.opened) {
+                        popup.completerName = "";
                         popup.close();
                     } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                         if (popup.opened) {
@@ -194,7 +195,10 @@ Rectangle {
                     } else if (event.key == Qt.Key_Tab) {
                         event.accepted = true;
                         if (popup.opened) {
-                            popup.up();
+                            if (event.modifiers & Qt.ShiftModifier)
+                                popup.down();
+                            else
+                                popup.up();
                         } else {
                             var pos = cursorPosition - 1;
                             while (pos > -1) {
@@ -218,7 +222,7 @@ Rectangle {
                     } else if (event.key == Qt.Key_Up && popup.opened) {
                         event.accepted = true;
                         popup.up();
-                    } else if (event.key == Qt.Key_Down && popup.opened) {
+                    } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Backtab) && popup.opened) {
                         event.accepted = true;
                         popup.down();
                     } else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
@@ -264,9 +268,8 @@ Rectangle {
                     function onRoomChanged() {
                         messageInput.clear();
                         if (room)
-                            messageInput.append(room.input.text());
+                            messageInput.append(room.input.text);
 
-                        messageInput.completerTriggeredAt = -1;
                         popup.completerName = "";
                         messageInput.forceActiveFocus();
                     }
@@ -285,8 +288,8 @@ Rectangle {
                 Completer {
                     id: popup
 
-                    x: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).x : 0
-                    y: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height : 0
+                    x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
+                    y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
                 }
 
                 Connections {
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index e5c6b4ecc7ae38e68bccee1c478a08f48fc09b0d..7ed30112987681d5103f4541295e93bf0be46a52 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -23,6 +23,8 @@ ScrollView {
 
         property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
 
+        displayMarginBeginning: height / 2
+        displayMarginEnd: height / 2
         model: room
         // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
         //onModelChanged: if (room) room.sendReset()
@@ -33,7 +35,7 @@ ScrollView {
         verticalLayoutDirection: ListView.BottomToTop
         onCountChanged: {
             // Mark timeline as read
-            if (atYEnd)
+            if (atYEnd && room)
                 model.currentIndex = 0;
 
         }
@@ -233,8 +235,8 @@ ScrollView {
                     id: dateBubble
 
                     anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-                    visible: previousMessageDay !== day
-                    text: chat.model.formatDateSeparator(timestamp)
+                    visible: room && previousMessageDay !== day
+                    text: room ? room.formatDateSeparator(timestamp) : ""
                     color: Nheko.colors.text
                     height: Math.round(fontMetrics.height * 1.4)
                     width: contentWidth * 1.2
@@ -257,10 +259,10 @@ ScrollView {
 
                         width: Nheko.avatarSize
                         height: Nheko.avatarSize
-                        url: chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
+                        url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
                         displayName: userName
                         userid: userId
-                        onClicked: chat.model.openUserProfile(userId)
+                        onClicked: room.openUserProfile(userId)
                         ToolTip.visible: avatarHover.hovered
                         ToolTip.text: userid
 
@@ -276,7 +278,7 @@ ScrollView {
                         }
 
                         function onScrollToIndex(index) {
-                            chat.positionViewAtIndex(index, ListView.Visible);
+                            chat.positionViewAtIndex(index, ListView.Center);
                         }
 
                         target: chat.model
@@ -361,7 +363,7 @@ ScrollView {
 
             anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
             width: chat.delegateMaxWidth
-            height: section ? section.height + timelinerow.height : timelinerow.height
+            height: Math.max(section.active ? section.height + timelinerow.height : timelinerow.height, 10)
 
             Rectangle {
                 id: scrollHighlight
@@ -420,6 +422,7 @@ ScrollView {
                 property string userName: wrapper.userName
                 property var timestamp: wrapper.timestamp
 
+                z: 4
                 active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day
                 //asynchronous: true
                 sourceComponent: sectionHeader
@@ -648,4 +651,39 @@ ScrollView {
 
     }
 
+    Platform.Menu {
+        id: replyContextMenu
+
+        property string text
+        property string link
+
+        function show(text_, link_) {
+            text = text_;
+            link = link_;
+            open();
+        }
+
+        Platform.MenuItem {
+            visible: replyContextMenu.text
+            enabled: visible
+            text: qsTr("&Copy")
+            onTriggered: Clipboard.text = replyContextMenu.text
+        }
+
+        Platform.MenuItem {
+            visible: replyContextMenu.link
+            enabled: visible
+            text: qsTr("Copy &link location")
+            onTriggered: Clipboard.text = replyContextMenu.link
+        }
+
+        Platform.MenuItem {
+            visible: true
+            enabled: visible
+            text: qsTr("&Go to quoted message")
+            onTriggered: chat.model.showEvent(eventId)
+        }
+
+    }
+
 }
diff --git a/resources/qml/NotificationWarning.qml b/resources/qml/NotificationWarning.qml
new file mode 100644
index 0000000000000000000000000000000000000000..75ef5f1767c876bcb086d81a7284d6c917160940
--- /dev/null
+++ b/resources/qml/NotificationWarning.qml
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Item {
+    implicitHeight: warningRect.visible ? warningDisplay.implicitHeight : 0
+    height: implicitHeight
+    Layout.fillWidth: true
+
+    Rectangle {
+        id: warningRect
+
+        visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
+        color: Nheko.colors.base
+        anchors.fill: parent
+        z: 3
+
+        Label {
+            id: warningDisplay
+
+            anchors.left: parent.left
+            anchors.leftMargin: 10
+            anchors.right: parent.right
+            anchors.rightMargin: 10
+            anchors.bottom: parent.bottom
+            color: Nheko.theme.red
+            text: qsTr("You are about to notify the whole room")
+            textFormat: Text.PlainText
+        }
+
+    }
+
+}
diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml
index defcc611ecc38f1debb86e6f6543dec5ca33d8aa..c7141c819b6d2520b221b70d6ec7866f377301a3 100644
--- a/resources/qml/QuickSwitcher.qml
+++ b/resources/qml/QuickSwitcher.qml
@@ -39,15 +39,15 @@ Popup {
             completerPopup.completer.searchString = text;
         }
         Keys.onPressed: {
-            if (event.key == Qt.Key_Up && completerPopup.opened) {
+            if ((event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) && completerPopup.opened) {
                 event.accepted = true;
                 completerPopup.up();
-            } else if (event.key == Qt.Key_Down && completerPopup.opened) {
+            } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
                 event.accepted = true;
-                completerPopup.down();
-            } else if (event.key == Qt.Key_Tab && completerPopup.opened) {
-                event.accepted = true;
-                completerPopup.down();
+                if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
+                    completerPopup.up();
+                else
+                    completerPopup.down();
             } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                 completerPopup.finishCompletion();
                 event.accepted = true;
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 8fbfce91337e7003d947286360709bd7e679a519..db255bd3c44674188b26a8ff6489d02fd347702a 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -16,6 +16,14 @@ Page {
     property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
     property bool collapsed: false
 
+    Component {
+        id: roomDirectoryComponent
+
+        RoomDirectory {
+        }
+
+    }
+
     ListView {
         id: roomlist
 
@@ -63,19 +71,9 @@ Page {
                 }
             }
 
-            Platform.MessageDialog {
-                id: leaveRoomDialog
-
-                title: qsTr("Leave Room")
-                text: qsTr("Are you sure you want to leave this room?")
-                modality: Qt.ApplicationModal
-                onAccepted: Rooms.leave(roomContextMenu.roomid)
-                buttons: Dialog.Ok | Dialog.Cancel
-            }
-
             Platform.MenuItem {
                 text: qsTr("Leave room")
-                onTriggered: leaveRoomDialog.open()
+                onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
             }
 
             Platform.MenuSeparator {
@@ -116,10 +114,10 @@ Page {
 
         }
 
-        delegate: Rectangle {
+        delegate: ItemDelegate {
             id: roomItem
 
-            property color background: Nheko.colors.window
+            property color backgroundColor: Nheko.colors.window
             property color importantText: Nheko.colors.text
             property color unimportantText: Nheko.colors.buttonText
             property color bubbleBackground: Nheko.colors.highlight
@@ -135,21 +133,34 @@ Page {
             required property int notificationCount
             required property bool hasLoudNotification
             required property bool hasUnreadMessages
+            required property bool isDirect
+            required property string directChatOtherUserId
 
-            color: background
             height: avatarSize + 2 * Nheko.paddingMedium
             width: ListView.view.width
             state: "normal"
-            ToolTip.visible: hovered.hovered && collapsed
+            ToolTip.visible: hovered && collapsed
             ToolTip.text: roomName
+            onClicked: {
+                console.log("tapped " + roomId);
+                if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
+                    Rooms.setCurrentRoom(roomId);
+                else
+                    Rooms.resetCurrentRoom();
+            }
+            onPressAndHold: {
+                if (!isInvite)
+                    roomContextMenu.show(roomId, tags);
+
+            }
             states: [
                 State {
                     name: "highlight"
-                    when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
+                    when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
 
                     PropertyChanges {
                         target: roomItem
-                        background: Nheko.colors.dark
+                        backgroundColor: Nheko.colors.dark
                         importantText: Nheko.colors.brightText
                         unimportantText: Nheko.colors.brightText
                         bubbleBackground: Nheko.colors.highlight
@@ -163,7 +174,7 @@ Page {
 
                     PropertyChanges {
                         target: roomItem
-                        background: Nheko.colors.highlight
+                        backgroundColor: Nheko.colors.highlight
                         importantText: Nheko.colors.highlightedText
                         unimportantText: Nheko.colors.highlightedText
                         bubbleBackground: Nheko.colors.highlightedText
@@ -189,27 +200,6 @@ Page {
                     acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
                 }
 
-                TapHandler {
-                    margin: -Nheko.paddingSmall
-                    onSingleTapped: {
-                        if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
-                            Rooms.setCurrentRoom(roomId);
-                        else
-                            Rooms.resetCurrentRoom();
-                    }
-                    onLongPressed: {
-                        if (!isInvite)
-                            roomContextMenu.show(roomId, tags);
-
-                    }
-                }
-
-                HoverHandler {
-                    id: hovered
-
-                    acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
-                }
-
             }
 
             RowLayout {
@@ -229,6 +219,8 @@ Page {
                     width: avatarSize
                     url: avatarUrl.replace("mxc://", "image://MxcImage/")
                     displayName: roomName
+                    userid: isDirect ? directChatOtherUserId : ""
+                    roomid: roomId
 
                     Rectangle {
                         id: collapsedNotificationBubble
@@ -359,6 +351,10 @@ Page {
                 visible: hasUnreadMessages
             }
 
+            background: Rectangle {
+                color: backgroundColor
+            }
+
         }
 
     }
@@ -500,6 +496,91 @@ Page {
             Layout.fillWidth: true
         }
 
+        Rectangle {
+            id: unverifiedStuffBubble
+
+            color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1)
+            Layout.fillWidth: true
+            implicitHeight: explanation.height + Nheko.paddingMedium * 2
+            visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified
+
+            RowLayout {
+                id: unverifiedStuffBubbleContainer
+
+                width: parent.width
+                height: explanation.height + Nheko.paddingMedium * 2
+                spacing: 0
+
+                Label {
+                    id: explanation
+
+                    Layout.margins: Nheko.paddingMedium
+                    Layout.rightMargin: Nheko.paddingSmall
+                    color: Nheko.colors.buttonText
+                    Layout.fillWidth: true
+                    text: {
+                        switch (SelfVerificationStatus.status) {
+                        case SelfVerificationStatus.NoMasterKey:
+                            //: Cross-signing setup has not run yet.
+                            return qsTr("Encryption not set up");
+                        case SelfVerificationStatus.UnverifiedMasterKey:
+                            //: The user just signed in with this device and hasn't verified their master key.
+                            return qsTr("Unverified login");
+                        case SelfVerificationStatus.UnverifiedDevices:
+                            //: There are unverified devices signed in to this account.
+                            return qsTr("Please verify your other devices");
+                        default:
+                            return "";
+                        }
+                    }
+                    textFormat: Text.PlainText
+                    wrapMode: Text.Wrap
+                }
+
+                ImageButton {
+                    id: closeUnverifiedBubble
+
+                    Layout.rightMargin: Nheko.paddingMedium
+                    Layout.topMargin: Nheko.paddingMedium
+                    Layout.alignment: Qt.AlignRight | Qt.AlignTop
+                    hoverEnabled: true
+                    width: fontMetrics.font.pixelSize
+                    height: fontMetrics.font.pixelSize
+                    image: ":/icons/icons/ui/remove-symbol.png"
+                    ToolTip.visible: closeUnverifiedBubble.hovered
+                    ToolTip.text: qsTr("Close")
+                    onClicked: unverifiedStuffBubble.visible = false
+                }
+
+            }
+
+            HoverHandler {
+                id: verifyButtonHovered
+
+                enabled: !closeUnverifiedBubble.hovered
+                acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+            }
+
+            TapHandler {
+                enabled: !closeUnverifiedBubble.hovered
+                acceptedButtons: Qt.LeftButton
+                onSingleTapped: {
+                    if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
+                        SelfVerificationStatus.verifyUnverifiedDevices();
+                    else
+                        SelfVerificationStatus.statusChanged();
+                }
+            }
+
+        }
+
+        Rectangle {
+            color: Nheko.theme.separator
+            height: 1
+            Layout.fillWidth: true
+            visible: unverifiedStuffBubble.visible
+        }
+
     }
 
     footer: ColumnLayout {
@@ -563,6 +644,10 @@ Page {
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("Room directory")
                     Layout.margins: Nheko.paddingMedium
+                    onClicked: {
+                        var win = roomDirectoryComponent.createObject(timelineRoot);
+                        win.show();
+                    }
                 }
 
                 ImageButton {
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index cc7d32ea47971374a56f67f0a5f7e1f5b86d4baf..f6b26041149b9b92a5744906010560984188708b 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -111,6 +111,30 @@ Page {
 
     }
 
+    Component {
+        id: logoutDialog
+
+        LogoutDialog {
+        }
+
+    }
+
+    Component {
+        id: joinRoomDialog
+
+        JoinRoomDialog {
+        }
+
+    }
+
+    Component {
+        id: leaveRoomComponent
+
+        LeaveRoomDialog {
+        }
+
+    }
+
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
@@ -120,6 +144,11 @@ Page {
         }
     }
 
+    Shortcut {
+        sequence: "Alt+A"
+        onActivated: Rooms.nextRoomWithActivity()
+    }
+
     Shortcut {
         sequence: "Ctrl+Down"
         onActivated: Rooms.nextRoom()
@@ -130,6 +159,20 @@ Page {
         onActivated: Rooms.previousRoom()
     }
 
+    Connections {
+        function onOpenLogoutDialog() {
+            var dialog = logoutDialog.createObject(timelineRoot);
+            dialog.open();
+        }
+
+        function onOpenJoinRoomDialog() {
+            var dialog = joinRoomDialog.createObject(timelineRoot);
+            dialog.show();
+        }
+
+        target: Nheko
+    }
+
     Connections {
         function onNewDeviceVerificationRequest(flow) {
             var dialog = deviceVerificationDialog.createObject(timelineRoot, {
@@ -138,6 +181,10 @@ Page {
             dialog.show();
         }
 
+        target: VerificationManager
+    }
+
+    Connections {
         function onOpenProfile(profile) {
             var userProfile = userProfileComponent.createObject(timelineRoot, {
                 "profile": profile
@@ -176,6 +223,13 @@ Page {
             dialog.show();
         }
 
+        function onOpenLeaveRoomDialog(roomid) {
+            var dialog = leaveRoomComponent.createObject(timelineRoot, {
+                "roomId": roomid
+            });
+            dialog.open();
+        }
+
         target: TimelineManager
     }
 
@@ -190,6 +244,94 @@ Page {
         target: CallManager
     }
 
+    SelfVerificationCheck {
+    }
+
+    InputDialog {
+        id: uiaPassPrompt
+
+        echoMode: TextInput.Password
+        title: UIA.title
+        prompt: qsTr("Please enter your login password to continue:")
+        onAccepted: (t) => {
+            return UIA.continuePassword(t);
+        }
+    }
+
+    InputDialog {
+        id: uiaEmailPrompt
+
+        title: UIA.title
+        prompt: qsTr("Please enter a valid email address to continue:")
+        onAccepted: (t) => {
+            return UIA.continueEmail(t);
+        }
+    }
+
+    PhoneNumberInputDialog {
+        id: uiaPhoneNumberPrompt
+
+        title: UIA.title
+        prompt: qsTr("Please enter a valid phone number to continue:")
+        onAccepted: (p, t) => {
+            return UIA.continuePhoneNumber(p, t);
+        }
+    }
+
+    InputDialog {
+        id: uiaTokenPrompt
+
+        title: UIA.title
+        prompt: qsTr("Please enter the token, which has been sent to you:")
+        onAccepted: (t) => {
+            return UIA.submit3pidToken(t);
+        }
+    }
+
+    Platform.MessageDialog {
+        id: uiaErrorDialog
+
+        buttons: Platform.MessageDialog.Ok
+    }
+
+    Platform.MessageDialog {
+        id: uiaConfirmationLinkDialog
+
+        buttons: Platform.MessageDialog.Ok
+        text: qsTr("Wait for the confirmation link to arrive, then continue.")
+        onAccepted: UIA.continue3pidReceived()
+    }
+
+    Connections {
+        function onPassword() {
+            console.log("UIA: password needed");
+            uiaPassPrompt.show();
+        }
+
+        function onEmail() {
+            uiaEmailPrompt.show();
+        }
+
+        function onPhoneNumber() {
+            uiaPhoneNumberPrompt.show();
+        }
+
+        function onPrompt3pidToken() {
+            uiaTokenPrompt.show();
+        }
+
+        function onConfirm3pidToken() {
+            uiaConfirmationLinkDialog.open();
+        }
+
+        function onError(msg) {
+            uiaErrorDialog.text = msg;
+            uiaErrorDialog.open();
+        }
+
+        target: UIA
+    }
+
     ChatPage {
         anchors.fill: parent
     }
diff --git a/resources/qml/SelfVerificationCheck.qml b/resources/qml/SelfVerificationCheck.qml
new file mode 100644
index 0000000000000000000000000000000000000000..23997e58c1bf65cf3aa9291642bbf717d7c32876
--- /dev/null
+++ b/resources/qml/SelfVerificationCheck.qml
@@ -0,0 +1,305 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "./components/"
+import Qt.labs.platform 1.1 as P
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+Item {
+    visible: false
+    enabled: false
+
+    Dialog {
+        id: showRecoverKeyDialog
+
+        property string recoveryKey: ""
+
+        parent: Overlay.overlay
+        anchors.centerIn: parent
+        height: content.height + implicitFooterHeight + implicitHeaderHeight
+        width: content.width
+        padding: 0
+        modal: true
+        standardButtons: Dialog.Ok
+        closePolicy: Popup.NoAutoClose
+
+        ColumnLayout {
+            id: content
+
+            spacing: 0
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
+                Layout.fillWidth: true
+                text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Do not pass go! Do not collect $200!")
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            TextEdit {
+                Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
+                Layout.alignment: Qt.AlignHCenter
+                horizontalAlignment: TextEdit.AlignHCenter
+                verticalAlignment: TextEdit.AlignVCenter
+                readOnly: true
+                selectByMouse: true
+                text: showRecoverKeyDialog.recoveryKey
+                color: Nheko.colors.text
+                font.bold: true
+                wrapMode: TextEdit.Wrap
+            }
+
+        }
+
+        background: Rectangle {
+            color: Nheko.colors.window
+            border.color: Nheko.theme.separator
+            border.width: 1
+            radius: Nheko.paddingSmall
+        }
+
+    }
+
+    P.MessageDialog {
+        id: successDialog
+
+        buttons: P.MessageDialog.Ok
+        text: qsTr("Encryption setup successfully")
+    }
+
+    P.MessageDialog {
+        id: failureDialog
+
+        property string errorMessage
+
+        buttons: P.MessageDialog.Ok
+        text: qsTr("Failed to setup encryption: %1").arg(errorMessage)
+    }
+
+    MainWindowDialog {
+        id: bootstrapCrosssigning
+
+        onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked)
+
+        GridLayout {
+            id: grid
+
+            width: bootstrapCrosssigning.useableWidth
+            columns: 2
+            rowSpacing: 0
+            columnSpacing: 0
+            z: 1
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.alignment: Qt.AlignHCenter
+                Layout.columnSpan: 2
+                font.pointSize: fontMetrics.font.pointSize * 2
+                text: qsTr("Setup Encryption")
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.alignment: Qt.AlignLeft
+                Layout.columnSpan: 2
+                Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
+                text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!")
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.alignment: Qt.AlignLeft
+                Layout.columnSpan: 1
+                Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
+                text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!"
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            Item {
+                Layout.margins: Nheko.paddingMedium
+                Layout.preferredHeight: storeSecretsOnline.height
+                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                Layout.fillWidth: true
+
+                ToggleButton {
+                    id: storeSecretsOnline
+
+                    checked: true
+                    onClicked: console.log("Store secrets toggled: " + checked)
+                }
+
+            }
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.alignment: Qt.AlignLeft
+                Layout.columnSpan: 1
+                Layout.rowSpan: 2
+                Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
+                visible: storeSecretsOnline.checked
+                text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)"
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            Item {
+                Layout.margins: Nheko.paddingMedium
+                Layout.topMargin: Nheko.paddingLarge
+                Layout.preferredHeight: storeSecretsOnline.height
+                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
+                Layout.rowSpan: usePassword.checked ? 1 : 2
+                Layout.fillWidth: true
+                visible: storeSecretsOnline.checked
+
+                ToggleButton {
+                    id: usePassword
+
+                    checked: false
+                }
+
+            }
+
+            MatrixTextField {
+                id: passwordField
+
+                Layout.margins: Nheko.paddingMedium
+                Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
+                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
+                Layout.columnSpan: 1
+                Layout.fillWidth: true
+                visible: storeSecretsOnline.checked && usePassword.checked
+                echoMode: TextInput.Password
+            }
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.alignment: Qt.AlignLeft
+                Layout.columnSpan: 1
+                Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
+                text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages."
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            Item {
+                Layout.margins: Nheko.paddingMedium
+                Layout.preferredHeight: storeSecretsOnline.height
+                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                Layout.fillWidth: true
+
+                ToggleButton {
+                    id: useOnlineKeyBackup
+
+                    checked: true
+                    onClicked: console.log("Online key backup toggled: " + checked)
+                }
+
+            }
+
+        }
+
+        background: Rectangle {
+            color: Nheko.colors.window
+            border.color: Nheko.theme.separator
+            border.width: 1
+            radius: Nheko.paddingSmall
+        }
+
+    }
+
+    MainWindowDialog {
+        id: verifyMasterKey
+
+        standardButtons: Dialog.Cancel
+
+        GridLayout {
+            id: masterGrid
+
+            width: verifyMasterKey.useableWidth
+            columns: 1
+            z: 1
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.alignment: Qt.AlignHCenter
+                //Layout.columnSpan: 2
+                font.pointSize: fontMetrics.font.pointSize * 2
+                text: qsTr("Activate Encryption")
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            Label {
+                Layout.margins: Nheko.paddingMedium
+                Layout.alignment: Qt.AlignLeft
+                //Layout.columnSpan: 2
+                Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
+                text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.")
+                color: Nheko.colors.text
+                wrapMode: Text.Wrap
+            }
+
+            FlatButton {
+                Layout.alignment: Qt.AlignHCenter
+                text: qsTr("verify")
+                onClicked: {
+                    SelfVerificationStatus.verifyMasterKey();
+                    verifyMasterKey.close();
+                }
+            }
+
+            FlatButton {
+                visible: SelfVerificationStatus.hasSSSS
+                Layout.alignment: Qt.AlignHCenter
+                text: qsTr("enter passphrase")
+                onClicked: {
+                    SelfVerificationStatus.verifyMasterKeyWithPassphrase();
+                    verifyMasterKey.close();
+                }
+            }
+
+        }
+
+    }
+
+    Connections {
+        function onStatusChanged() {
+            console.log("STATUS CHANGED: " + SelfVerificationStatus.status);
+            if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) {
+                bootstrapCrosssigning.open();
+            } else if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedMasterKey) {
+                verifyMasterKey.open();
+            } else {
+                bootstrapCrosssigning.close();
+                verifyMasterKey.close();
+            }
+        }
+
+        function onShowRecoveryKey(key) {
+            showRecoverKeyDialog.recoveryKey = key;
+            showRecoverKeyDialog.open();
+        }
+
+        function onSetupCompleted() {
+            successDialog.open();
+        }
+
+        function onSetupFailed(m) {
+            failureDialog.errorMessage = m;
+            failureDialog.open();
+        }
+
+        target: SelfVerificationStatus
+    }
+
+}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index c8ac6bc7be1963bb90d0b1259d1dd07a82def408..8214d9de1602d0980b76c1981baaf8b457059cde 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -24,7 +24,7 @@ Item {
     property bool showBackButton: false
 
     Label {
-        visible: !room && !TimelineManager.isInitialSync && !roomPreview
+        visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
         anchors.centerIn: parent
         text: qsTr("No room open")
         font.pointSize: 24
@@ -84,14 +84,9 @@ Item {
                         target: timelineView
                     }
 
-                    Loader {
-                        active: room || roomPreview
+                    MessageView {
+                        implicitHeight: msgView.height - typingIndicator.height
                         Layout.fillWidth: true
-
-                        sourceComponent: MessageView {
-                            implicitHeight: msgView.height - typingIndicator.height
-                        }
-
                     }
 
                     Loader {
@@ -128,6 +123,9 @@ Item {
             color: Nheko.theme.separator
         }
 
+        NotificationWarning {
+        }
+
         ReplyPopup {
         }
 
@@ -139,6 +137,7 @@ Item {
     ColumnLayout {
         id: preview
 
+        property string roomId: room ? room.roomId : (roomPreview ? roomPreview.roomid : "")
         property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "")
         property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "")
         property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "")
@@ -155,6 +154,7 @@ Item {
 
         Avatar {
             url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
+            roomid: parent.roomId
             displayName: parent.roomName
             height: 130
             width: 130
@@ -163,7 +163,7 @@ Item {
         }
 
         MatrixText {
-            text: parent.roomName
+            text: parent.roomName == "" ? qsTr("No preview available") : parent.roomName
             font.pixelSize: 24
             Layout.alignment: Qt.AlignHCenter
         }
@@ -240,7 +240,7 @@ Item {
         anchors.margins: Nheko.paddingMedium
         width: Nheko.avatarSize
         height: Nheko.avatarSize
-        visible: room != null && room.isSpace && showBackButton
+        visible: (room == null || room.isSpace) && showBackButton
         enabled: visible
         image: ":/icons/icons/ui/angle-pointing-to-left.png"
         ToolTip.visible: hovered
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 7f67c028dc4bf94d0361f335acb7c14b6a2a90b5..e9f482c9b5addf6d50a2bafcfad39d9946b74d80 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -13,10 +13,13 @@ Rectangle {
 
     property bool showBackButton: false
     property string roomName: room ? room.roomName : qsTr("No room selected")
+    property string roomId: room ? room.roomId : ""
     property string avatarUrl: room ? room.roomAvatarUrl : ""
     property string roomTopic: room ? room.roomTopic : ""
     property bool isEncrypted: room ? room.isEncrypted : false
     property int trustlevel: room ? room.trustlevel : Crypto.Unverified
+    property bool isDirect: room ? room.isDirect : false
+    property string directChatOtherUserId: room ? room.directChatOtherUserId : ""
 
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
@@ -65,10 +68,12 @@ Rectangle {
             width: Nheko.avatarSize
             height: Nheko.avatarSize
             url: avatarUrl.replace("mxc://", "image://MxcImage/")
+            roomid: roomId
+            userid: isDirect ? directChatOtherUserId : ""
             displayName: roomName
             onClicked: {
                 if (room)
-                    TimelineManager.openRoomSettings(room.roomId);
+                    TimelineManager.openRoomSettings(roomId);
 
             }
         }
@@ -109,7 +114,7 @@ Rectangle {
                 case Crypto.Verified:
                     return qsTr("This room contains only verified devices.");
                 case Crypto.TOFU:
-                    return qsTr("This rooms contain verified devices and devices which have never changed their master key.");
+                    return qsTr("This room contains verified devices and devices which have never changed their master key.");
                 default:
                     return qsTr("This room contains unverified devices!");
                 }
@@ -135,7 +140,7 @@ Rectangle {
                 Platform.MenuItem {
                     visible: room ? room.permissions.canInvite() : false
                     text: qsTr("Invite users")
-                    onTriggered: TimelineManager.openInviteUsers(room.roomId)
+                    onTriggered: TimelineManager.openInviteUsers(roomId)
                 }
 
                 Platform.MenuItem {
@@ -145,12 +150,12 @@ Rectangle {
 
                 Platform.MenuItem {
                     text: qsTr("Leave room")
-                    onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId)
+                    onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Settings")
-                    onTriggered: TimelineManager.openRoomSettings(room.roomId)
+                    onTriggered: TimelineManager.openRoomSettings(roomId)
                 }
 
             }
diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml
deleted file mode 100644
index 767d23173a81da598e5a2d7514d9d38ba1e27a47..0000000000000000000000000000000000000000
--- a/resources/qml/UserProfile.qml
+++ /dev/null
@@ -1,270 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import "./device-verification"
-import "./ui"
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.2
-import QtQuick.Window 2.13
-import im.nheko 1.0
-
-ApplicationWindow {
-    // this does not work in ApplicationWindow, just in Window
-    //transientParent: Nheko.mainwindow()
-
-    id: userProfileDialog
-
-    property var profile
-
-    height: 650
-    width: 420
-    minimumHeight: 420
-    palette: Nheko.colors
-    color: Nheko.colors.window
-    title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
-    modality: Qt.NonModal
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
-    Component.onCompleted: Nheko.reparent(userProfileDialog)
-
-    Shortcut {
-        sequence: StandardKey.Cancel
-        onActivated: userProfileDialog.close()
-    }
-
-    ColumnLayout {
-        id: contentL
-
-        anchors.fill: parent
-        anchors.margins: 10
-        spacing: 10
-
-        Avatar {
-            url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
-            height: 130
-            width: 130
-            displayName: profile.displayName
-            userid: profile.userid
-            Layout.alignment: Qt.AlignHCenter
-            onClicked: profile.isSelf ? profile.changeAvatar() : TimelineManager.openImageOverlay(profile.avatarUrl, "")
-        }
-
-        Spinner {
-            Layout.alignment: Qt.AlignHCenter
-            running: profile.isLoading
-            visible: profile.isLoading
-            foreground: Nheko.colors.mid
-        }
-
-        Text {
-            id: errorText
-
-            color: "red"
-            visible: opacity > 0
-            opacity: 0
-            Layout.alignment: Qt.AlignHCenter
-        }
-
-        SequentialAnimation {
-            id: hideErrorAnimation
-
-            running: false
-
-            PauseAnimation {
-                duration: 4000
-            }
-
-            NumberAnimation {
-                target: errorText
-                property: 'opacity'
-                to: 0
-                duration: 1000
-            }
-
-        }
-
-        Connections {
-            function onDisplayError(errorMessage) {
-                errorText.text = errorMessage;
-                errorText.opacity = 1;
-                hideErrorAnimation.restart();
-            }
-
-            target: profile
-        }
-
-        TextInput {
-            id: displayUsername
-
-            property bool isUsernameEditingAllowed
-
-            readOnly: !isUsernameEditingAllowed
-            text: profile.displayName
-            font.pixelSize: 20
-            color: TimelineManager.userColor(profile.userid, Nheko.colors.window)
-            font.bold: true
-            Layout.alignment: Qt.AlignHCenter
-            selectByMouse: true
-            onAccepted: {
-                profile.changeUsername(displayUsername.text);
-                displayUsername.isUsernameEditingAllowed = false;
-            }
-
-            ImageButton {
-                visible: profile.isSelf
-                anchors.leftMargin: 5
-                anchors.left: displayUsername.right
-                anchors.verticalCenter: displayUsername.verticalCenter
-                image: displayUsername.isUsernameEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
-                onClicked: {
-                    if (displayUsername.isUsernameEditingAllowed) {
-                        profile.changeUsername(displayUsername.text);
-                        displayUsername.isUsernameEditingAllowed = false;
-                    } else {
-                        displayUsername.isUsernameEditingAllowed = true;
-                        displayUsername.focus = true;
-                        displayUsername.selectAll();
-                    }
-                }
-            }
-
-        }
-
-        MatrixText {
-            text: profile.userid
-            font.pixelSize: 15
-            Layout.alignment: Qt.AlignHCenter
-        }
-
-        Button {
-            id: verifyUserButton
-
-            text: qsTr("Verify")
-            Layout.alignment: Qt.AlignHCenter
-            enabled: profile.userVerified != Crypto.Verified
-            visible: profile.userVerified != Crypto.Verified && !profile.isSelf && profile.userVerificationEnabled
-            onClicked: profile.verify()
-        }
-
-        Image {
-            Layout.preferredHeight: 16
-            Layout.preferredWidth: 16
-            source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : Nheko.colors.buttonText)
-            visible: profile.userVerified != Crypto.Unverified
-            Layout.alignment: Qt.AlignHCenter
-        }
-
-        RowLayout {
-            // ImageButton{
-            //     image:":/icons/icons/ui/volume-off-indicator.png"
-            //     Layout.margins: {
-            //         left: 5
-            //         right: 5
-            //     }
-            //     ToolTip.visible: hovered
-            //     ToolTip.text: qsTr("Ignore messages from this user")
-            //     onClicked : {
-            //         profile.ignoreUser()
-            //     }
-            // }
-
-            Layout.alignment: Qt.AlignHCenter
-            spacing: 8
-
-            ImageButton {
-                image: ":/icons/icons/ui/black-bubble-speech.png"
-                hoverEnabled: true
-                ToolTip.visible: hovered
-                ToolTip.text: qsTr("Start a private chat")
-                onClicked: profile.startChat()
-            }
-
-            ImageButton {
-                image: ":/icons/icons/ui/round-remove-button.png"
-                hoverEnabled: true
-                ToolTip.visible: hovered
-                ToolTip.text: qsTr("Kick the user")
-                onClicked: profile.kickUser()
-                visible: profile.room ? profile.room.permissions.canKick() : false
-            }
-
-            ImageButton {
-                image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
-                hoverEnabled: true
-                ToolTip.visible: hovered
-                ToolTip.text: qsTr("Ban the user")
-                onClicked: profile.banUser()
-                visible: profile.room ? profile.room.permissions.canBan() : false
-            }
-
-        }
-
-        ListView {
-            id: devicelist
-
-            Layout.fillHeight: true
-            Layout.minimumHeight: 200
-            Layout.fillWidth: true
-            clip: true
-            spacing: 8
-            boundsBehavior: Flickable.StopAtBounds
-            model: profile.deviceList
-
-            delegate: RowLayout {
-                width: devicelist.width
-                spacing: 4
-
-                ColumnLayout {
-                    spacing: 0
-
-                    Text {
-                        Layout.fillWidth: true
-                        Layout.alignment: Qt.AlignLeft
-                        elide: Text.ElideRight
-                        font.bold: true
-                        color: Nheko.colors.text
-                        text: model.deviceId
-                    }
-
-                    Text {
-                        Layout.fillWidth: true
-                        Layout.alignment: Qt.AlignRight
-                        elide: Text.ElideRight
-                        color: Nheko.colors.text
-                        text: model.deviceName
-                    }
-
-                }
-
-                Image {
-                    Layout.preferredHeight: 16
-                    Layout.preferredWidth: 16
-                    source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red"))
-                }
-
-                Button {
-                    id: verifyButton
-
-                    visible: (!profile.userVerificationEnabled && !profile.isSelf) || (profile.isSelf && (model.verificationStatus != VerificationStatus.VERIFIED || !profile.userVerificationEnabled))
-                    text: (model.verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
-                    onClicked: {
-                        if (model.verificationStatus == VerificationStatus.VERIFIED)
-                            profile.unverify(model.deviceId);
-                        else
-                            profile.verify(model.deviceId);
-                    }
-                }
-
-            }
-
-        }
-
-    }
-
-    footer: DialogButtonBox {
-        standardButtons: DialogButtonBox.Ok
-        onAccepted: userProfileDialog.close()
-    }
-
-}
diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml
index 36c26a9780024ae1ab333c93ecfa76adabce9e05..853266c64667f0d2bf17674407af735f20490d1b 100644
--- a/resources/qml/components/AvatarListTile.qml
+++ b/resources/qml/components/AvatarListTile.qml
@@ -23,6 +23,8 @@ Rectangle {
     required property int index
     required property int selectedIndex
     property bool crop: true
+    property alias roomid: avatar.roomid
+    property alias userid: avatar.userid
 
     color: background
     height: avatarSize + 2 * Nheko.paddingMedium
diff --git a/resources/qml/components/MainWindowDialog.qml b/resources/qml/components/MainWindowDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..901260b5353d912c8d7d7c36eb7a5722229c5352
--- /dev/null
+++ b/resources/qml/components/MainWindowDialog.qml
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Qt.labs.platform 1.1 as P
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+Dialog {
+    default property alias inner: scroll.data
+    property int useableWidth: scroll.width - scroll.ScrollBar.vertical.width
+
+    parent: Overlay.overlay
+    anchors.centerIn: parent
+    height: (Math.floor(parent.height / 2) - Nheko.paddingLarge) * 2
+    width: (Math.floor(parent.width / 2) - Nheko.paddingLarge) * 2
+    padding: 0
+    modal: true
+    standardButtons: Dialog.Ok | Dialog.Cancel
+    closePolicy: Popup.NoAutoClose
+    contentChildren: [
+        ScrollView {
+            id: scroll
+
+            clip: true
+            anchors.fill: parent
+            ScrollBar.horizontal.visible: false
+            ScrollBar.vertical.visible: true
+        }
+    ]
+
+    background: Rectangle {
+        color: Nheko.colors.window
+        border.color: Nheko.theme.separator
+        border.width: 1
+        radius: Nheko.paddingSmall
+    }
+
+}
diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml
index cd00a9d4c182dff696037a60bbfcb28f750d2cd9..6616d3cedfc6c01559b3d74983c8bdd52ba179b4 100644
--- a/resources/qml/delegates/Encrypted.qml
+++ b/resources/qml/delegates/Encrypted.qml
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import ".."
+import QtQuick 2.15
 import QtQuick.Controls 2.1
-import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
-ColumnLayout {
+Column {
     id: r
 
     required property int encryptionError
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index b432018c7753c065fc6bb9264a471b68fdeb36de..64e365c8be1355611b796d7623c50e83cda95fcf 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -14,6 +14,7 @@ Item {
     required property string body
     required property string filename
     required property bool isReply
+    required property string eventId
     property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth)
     property double tempHeight: tempWidth * proportionalHeight
     property double divisor: isReply ? 5 : 3
@@ -37,6 +38,7 @@ Item {
     Image {
         id: img
 
+        visible: !mxcimage.loaded
         anchors.fill: parent
         source: url.replace("mxc://", "image://MxcImage/")
         asynchronous: true
@@ -53,38 +55,48 @@ Item {
             gesturePolicy: TapHandler.ReleaseWithinBounds
         }
 
-        HoverHandler {
-            id: mouseArea
-        }
+    }
 
-        Item {
-            id: overlay
+    MxcAnimatedImage {
+        id: mxcimage
 
-            anchors.fill: parent
-            visible: mouseArea.hovered
+        visible: loaded
+        anchors.fill: parent
+        roomm: room
+        play: !Settings.animateImagesOnHover || mouseArea.hovered
+        eventId: parent.eventId
+    }
 
-            Rectangle {
-                id: container
+    HoverHandler {
+        id: mouseArea
+    }
 
-                width: parent.width
-                implicitHeight: imgcaption.implicitHeight
-                anchors.bottom: overlay.bottom
-                color: Nheko.colors.window
-                opacity: 0.75
-            }
+    Item {
+        id: overlay
 
-            Text {
-                id: imgcaption
+        anchors.fill: parent
+        visible: mouseArea.hovered
 
-                anchors.fill: container
-                elide: Text.ElideMiddle
-                horizontalAlignment: Text.AlignHCenter
-                verticalAlignment: Text.AlignVCenter
-                // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
-                text: filename ? filename : body
-                color: Nheko.colors.text
-            }
+        Rectangle {
+            id: container
+
+            width: parent.width
+            implicitHeight: imgcaption.implicitHeight
+            anchors.bottom: overlay.bottom
+            color: Nheko.colors.window
+            opacity: 0.75
+        }
+
+        Text {
+            id: imgcaption
 
+            anchors.fill: container
+            elide: Text.ElideMiddle
+            horizontalAlignment: Text.AlignHCenter
+            verticalAlignment: Text.AlignVCenter
+            // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
+            text: filename ? filename : body
+            color: Nheko.colors.text
         }
 
     }
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index a8bdf18316ccda944b3bb5973a5fe6c7d39a7c5a..9f889106a3107270c1453e8d653049c9b310c1b7 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -3,6 +3,8 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import QtQuick 2.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
 Item {
@@ -32,7 +34,7 @@ Item {
     required property int encryptionError
     required property int relatedEventCacheBuster
 
-    height: chooser.childrenRect.height
+    height: Math.max(chooser.child.height, 20)
 
     DelegateChooser {
         id: chooser
@@ -100,6 +102,7 @@ Item {
                 body: d.body
                 filename: d.filename
                 isReply: d.isReply
+                eventId: d.eventId
             }
 
         }
@@ -116,6 +119,7 @@ Item {
                 body: d.body
                 filename: d.filename
                 isReply: d.isReply
+                eventId: d.eventId
             }
 
         }
@@ -357,11 +361,23 @@ Item {
         DelegateChoice {
             roleValue: MtxEvent.Member
 
-            NoticeMessage {
-                body: formatted
-                isOnlyEmoji: false
-                isReply: d.isReply
-                formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId)
+            ColumnLayout {
+                width: parent ? parent.width : undefined
+
+                NoticeMessage {
+                    body: formatted
+                    isOnlyEmoji: false
+                    isReply: d.isReply
+                    formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId)
+                }
+
+                Button {
+                    visible: d.relatedEventCacheBuster, room.showAcceptKnockButton(d.eventId)
+                    palette: Nheko.colors
+                    text: qsTr("Allow them in")
+                    onClicked: room.acceptKnock(eventId)
+                }
+
             }
 
         }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index aa7ee5306678cbdc4c380d96c1d9238a2aaecf50..fbc4a637ece98e797652bc250112df70ede67909 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -3,9 +3,9 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import "../"
-import QtMultimedia 5.6
-import QtQuick 2.12
-import QtQuick.Controls 2.1
+import QtMultimedia 5.15
+import QtQuick 2.15
+import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
@@ -40,26 +40,16 @@ ColumnLayout {
 
     id: content
     Layout.maximumWidth: parent? parent.width: undefined
-    MediaPlayer {
-        id: media
+    MxcMedia {
+        id: mxcmedia
         // TODO: Show error in overlay or so?
-        onError: console.log(errorString)
-        volume: volumeSlider.desiredVolume
-    }
-
-    Connections {
-        property bool mediaCached: false
-
-        id: mediaCachedObserver
-        target: room
-        function onMediaCached(mxcUrl, cacheUrl) {
-            if (mxcUrl == url) {
-                mediaCached = true
-                media.source = "file://" + cacheUrl
-                console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
-            }
-            console.log("media cached: " + mxcUrl + " at " + cacheUrl)
-        }
+        onError: console.log(error)
+        roomm: room
+		onMediaStatusChanged: {
+			if (status == MxcMedia.LoadedMedia) {
+				progress.updatePositionTexts();
+			}
+		}
     }
       
     Rectangle {
@@ -87,7 +77,7 @@ ColumnLayout {
             Rectangle {
                 // Display over video controls
                 z: videoOutput.z + 1
-                visible: !mediaCachedObserver.mediaCached
+                visible: !mxcmedia.loaded
                 anchors.fill: parent
                 color: Nheko.colors.window
                 opacity: 0.5
@@ -103,8 +93,8 @@ ColumnLayout {
                     id: cacheVideoArea
                     anchors.fill: parent
                     hoverEnabled: true
-                    enabled: !mediaCachedObserver.mediaCached
-                    onClicked: room.cacheMedia(eventId)
+                    enabled: !mxcmedia.loaded
+                    onClicked: mxcmedia.eventId = eventId
                 }
             }
             VideoOutput {
@@ -112,7 +102,9 @@ ColumnLayout {
                 clip: true
                 anchors.fill: parent
                 fillMode: VideoOutput.PreserveAspectFit
-                source: media
+                source: mxcmedia
+				flushMode: VideoOutput.FirstFrame
+
                 // TODO: once we can use Qt 5.12, use HoverHandler
                 MouseArea {
                     id: playerMouseArea
@@ -120,9 +112,9 @@ ColumnLayout {
                     onClicked: {
                         if (controlRect.shouldShowControls &&
                             !controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) {
-                                (media.playbackState == MediaPlayer.PlayingState) ?
-                                    media.pause() :
-                                    media.play()
+                                (mxcmedia.state == MediaPlayer.PlayingState) ?
+                                    mxcmedia.pause() :
+                                    mxcmedia.play()
                         }
                     }
 					Rectangle {
@@ -159,7 +151,7 @@ ColumnLayout {
 								property color controlColor: (playbackStateArea.containsMouse) ?
 									Nheko.colors.highlight : Nheko.colors.text
 
-								source: (media.playbackState == MediaPlayer.PlayingState) ?
+								source: (mxcmedia.state == MediaPlayer.PlayingState) ?
 									"image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor :
 									"image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor
 								MouseArea {
@@ -168,25 +160,25 @@ ColumnLayout {
 									anchors.fill: parent
 									hoverEnabled: true
 									onClicked: {
-										(media.playbackState == MediaPlayer.PlayingState) ?
-											media.pause() :
-											media.play()
+										(mxcmedia.state == MediaPlayer.PlayingState) ?
+											mxcmedia.pause() :
+											mxcmedia.play()
 									}
 								}
 							}
 							Label {
-								text: (!mediaCachedObserver.mediaCached) ? "-/-" :
-									durationToString(media.position) + "/" + durationToString(media.duration)
+								text: (!mxcmedia.loaded) ? "-/-" :
+									durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
 							}
 
 							Slider {
 								Layout.fillWidth: true
 								Layout.minimumWidth: 50
 								height: controlRect.controlHeight
-								value: media.position
-								onMoved: media.seek(value)
+								value: mxcmedia.position
+								onMoved: mxcmedia.position = value
 								from: 0
-								to: media.duration
+								to: mxcmedia.duration
 							}
 							// Volume slider activator
 							Image {
@@ -195,7 +187,7 @@ ColumnLayout {
 
 								// TODO: add icons for different volume levels
 								id: volumeImage
-								source: (media.volume > 0 && !media.muted) ?
+								source: (mxcmedia.volume > 0 && !mxcmedia.muted) ?
 									"image://colorimage/:/icons/icons/ui/volume-up.png?"+ controlColor :
 									"image://colorimage/:/icons/icons/ui/volume-off-indicator.png?"+ controlColor
 								Layout.rightMargin: 5
@@ -205,7 +197,7 @@ ColumnLayout {
 									id: volumeImageArea	
 									anchors.fill: parent
 									hoverEnabled: true
-									onClicked: media.muted = !media.muted
+									onClicked: mxcmedia.muted = !mxcmedia.muted
 									onExited: volumeSliderHideTimer.start()
 									onPositionChanged: volumeSliderHideTimer.start()
 									// For hiding volume slider after a while
@@ -248,7 +240,7 @@ ColumnLayout {
 										id: volumeSlider
 										from: 0
 										to: 1
-										value: (media.muted) ? 0 :
+										value: (mxcmedia.muted) ? 0 :
 											QtMultimedia.convertVolume(desiredVolume,
 												QtMultimedia.LinearVolumeScale,
 												QtMultimedia.LogarithmicVolumeScale)
@@ -262,7 +254,7 @@ ColumnLayout {
 											QtMultimedia.LinearVolumeScale)
 										/* This would be better handled in 'media', but it has some issue with listening
 											to this signal */
-										onDesiredVolumeChanged: media.muted = !(desiredVolume > 0)
+										onDesiredVolumeChanged: mxcmedia.muted = !(desiredVolume > 0)
 									}
 									// Used for resetting the timer on mouse moves on volumeSliderRect
 									MouseArea {
@@ -288,7 +280,7 @@ ColumnLayout {
 					}
                     // This breaks separation of concerns but this same thing doesn't work when called from controlRect...
                     property bool shouldShowControls: (containsMouse && controlHideTimer.running) ||
-                        (media.playbackState != MediaPlayer.PlayingState) ||
+                        (mxcmedia.state != MediaPlayer.PlayingState) ||
                         controlRect.contains(mapToItem(controlRect, mouseX, mouseY))
 
                     // For hiding controls on stationary cursor
@@ -331,9 +323,9 @@ ColumnLayout {
 					Nheko.colors.highlight : Nheko.colors.text
 
 				source: {
-                    if (!mediaCachedObserver.mediaCached)
+                    if (!mxcmedia.loaded)
                         return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+controlColor
-                    return (media.playbackState == MediaPlayer.PlayingState) ?
+                    return (mxcmedia.state == MediaPlayer.PlayingState) ?
                         "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor :
                         "image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor
                 }
@@ -343,29 +335,29 @@ ColumnLayout {
 					anchors.fill: parent
 					hoverEnabled: true
 					onClicked: {
-                        if (!mediaCachedObserver.mediaCached) {
-                            room.cacheMedia(eventId)
+                        if (!mxcmedia.loaded) {
+                            mxcmedia.eventId = eventId
                             return
                         }
-						(media.playbackState == MediaPlayer.PlayingState) ?
-							media.pause() :
-							media.play()
+						(mxcmedia.state == MediaPlayer.PlayingState) ?
+							mxcmedia.pause() :
+							mxcmedia.play()
 					}
 				}
 			}
 			Label {
-				text: (!mediaCachedObserver.mediaCached) ? "-/-" :
-					durationToString(media.position) + "/" + durationToString(media.duration)
+				text: (!mxcmedia.loaded) ? "-/-" :
+					durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
 			}
 
 			Slider {
 				Layout.fillWidth: true
 				Layout.minimumWidth: 50
 				height: controlRect.controlHeight
-				value: media.position
-				onMoved: media.seek(value)
+				value: mxcmedia.position
+				onMoved: mxcmedia.seek(value)
 				from: 0
-				to: media.duration
+				to: mxcmedia.duration
 			}
 		}
 	}
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 8bbce10ee567a22f555e45594e141f00a762da9e..601548372ec2ea798bf1745a9f957bbbda1da2a1 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+import Qt.labs.platform 1.1 as Platform
 import QtQuick 2.12
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
@@ -36,11 +37,6 @@ Item {
     width: parent.width
     height: replyContainer.height
 
-    TapHandler {
-        onSingleTapped: chat.model.showEvent(eventId)
-        gesturePolicy: TapHandler.ReleaseWithinBounds
-    }
-
     CursorShape {
         anchors.fill: parent
         cursorShape: Qt.PointingHandCursor
@@ -62,6 +58,19 @@ Item {
         anchors.leftMargin: 4
         width: parent.width - 8
 
+        TapHandler {
+            acceptedButtons: Qt.LeftButton
+            onSingleTapped: chat.model.showEvent(r.eventId)
+            gesturePolicy: TapHandler.ReleaseWithinBounds
+        }
+
+        TapHandler {
+            acceptedButtons: Qt.RightButton
+            onLongPressed: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
+            onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
+            gesturePolicy: TapHandler.ReleaseWithinBounds
+        }
+
         Text {
             id: userName_
 
@@ -99,6 +108,7 @@ Item {
             callType: r.callType
             relatedEventCacheBuster: r.relatedEventCacheBuster
             encryptionError: r.encryptionError
+            // This is disabled so that left clicking the reply goes to its location
             enabled: false
             width: parent.width
             isReply: true
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index 58aa99cac551a7a9faf49aa64cff82145a960558..11ad3aeb1a6689605d2b9078bae1bc0b44763f8e 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import ".."
+import QtQuick.Controls 2.3
 import im.nheko 1.0
 
 MatrixText {
@@ -28,6 +29,7 @@ MatrixText {
         border-collapse: collapse;
         border: 1px solid " + Nheko.colors.text + ";
     }
+    blockquote { margin-left: 1em; }
     </style>
     " + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>").replace("<del>", "<s>").replace("</del>", "</s>").replace("<strike>", "<s>").replace("</strike>", "</s>")
     width: parent ? parent.width : undefined
@@ -35,4 +37,11 @@ MatrixText {
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
+
+    CursorShape {
+        enabled: isReply
+        anchors.fill: parent
+        cursorShape: Qt.PointingHandCursor
+    }
+
 }
diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
index 8e0271d689de8457ff1782116eb2199a6d568c60..5bc8b9c85918ddaf06185daa3e683727a37f27b9 100644
--- a/resources/qml/device-verification/DeviceVerification.qml
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -12,13 +12,13 @@ ApplicationWindow {
 
     property var flow
 
-    onClosing: TimelineManager.removeVerificationFlow(flow)
+    onClosing: VerificationManager.removeVerificationFlow(flow)
     title: stack.currentItem.title
     modality: Qt.NonModal
     palette: Nheko.colors
     height: stack.implicitHeight
     width: stack.implicitWidth
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     Component.onCompleted: Nheko.reparent(dialog)
 
     StackView {
diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml
index 71ef8b9b8f83fd646c30aa78fd317682fbaefd1a..6bfd4aedba461c5ea300cfbdb5d133e4b1705002 100644
--- a/resources/qml/device-verification/Failed.qml
+++ b/resources/qml/device-verification/Failed.qml
@@ -33,9 +33,9 @@ Pane {
                 case DeviceVerificationFlow.User:
                     return qsTr("Other party canceled the verification.");
                 case DeviceVerificationFlow.OutOfOrder:
-                    return qsTr("Device verification timed out.");
+                    return qsTr("Verification messages received out of order!");
                 default:
-                    return "Unknown verification error.";
+                    return qsTr("Unknown verification error.");
                 }
             }
             color: Nheko.colors.text
diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml
index 5ae2d25b25c6e5e835147af5a5825e6e69410f1b..7e521605fed21df5ce458ea3f0b5392145278b41 100644
--- a/resources/qml/device-verification/NewVerificationRequest.qml
+++ b/resources/qml/device-verification/NewVerificationRequest.qml
@@ -23,7 +23,10 @@ Pane {
             text: {
                 if (flow.sender) {
                     if (flow.isSelfVerification)
-                        return qsTr("To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?").arg(flow.deviceId);
+                        if (flow.isMultiDeviceVerification)
+                            return qsTr("To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)");
+                        else
+                            return qsTr("To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?").arg(flow.deviceId);
                     else
                         return qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.");
                 } else {
diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml
index b0f431f6c20fac599e4403d2af4aa71849f8c311..1db5d45f52147e2e1cc3ddd66fb039d48ec5e251 100644
--- a/resources/qml/dialogs/ImagePackEditorDialog.qml
+++ b/resources/qml/dialogs/ImagePackEditorDialog.qml
@@ -27,7 +27,7 @@ ApplicationWindow {
     palette: Nheko.colors
     color: Nheko.colors.base
     modality: Qt.WindowModal
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
 
     AdaptiveLayout {
         id: adaptiveView
@@ -61,6 +61,7 @@ ApplicationWindow {
                 header: AvatarListTile {
                     title: imagePack.packname
                     avatarUrl: imagePack.avatarUrl
+                    roomid: imagePack.statekey
                     subtitle: imagePack.statekey
                     index: -1
                     selectedIndex: currentImageIndex
@@ -90,7 +91,7 @@ ApplicationWindow {
 
                         folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
                         fileMode: FileDialog.OpenFiles
-                        nameFilters: [qsTr("Stickers (*.png *.webp)")]
+                        nameFilters: [qsTr("Stickers (*.png *.webp *.gif *.jpg *.jpeg)")]
                         onAccepted: imagePack.addStickers(files)
                     }
 
@@ -142,6 +143,7 @@ ApplicationWindow {
                         Layout.columnSpan: 2
                         url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/")
                         displayName: imagePack.packname
+                        roomid: imagePack.statekey
                         height: 130
                         width: 130
                         crop: false
@@ -219,6 +221,7 @@ ApplicationWindow {
                         Layout.columnSpan: 2
                         url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/")
                         displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
+                        roomid: displayName
                         height: 130
                         width: 130
                         crop: false
@@ -265,6 +268,20 @@ ApplicationWindow {
                         Layout.alignment: Qt.AlignRight
                     }
 
+                    MatrixText {
+                        text: qsTr("Remove from pack")
+                    }
+
+                    Button {
+                        text: qsTr("Remove")
+                        onClicked: {
+                            let temp = currentImageIndex;
+                            currentImageIndex = -1;
+                            imagePack.remove(temp);
+                        }
+                        Layout.alignment: Qt.AlignRight
+                    }
+
                     Item {
                         Layout.columnSpan: 2
                         Layout.fillHeight: true
diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml
index 5181619c03ca620323743d09e12cdfcfad171e15..e48040c1f42dd98653948facc170824d0c401d99 100644
--- a/resources/qml/dialogs/ImagePackSettingsDialog.qml
+++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml
@@ -25,7 +25,7 @@ ApplicationWindow {
     palette: Nheko.colors
     color: Nheko.colors.base
     modality: Qt.NonModal
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     Component.onCompleted: Nheko.reparent(win)
 
     Component {
@@ -101,6 +101,7 @@ ApplicationWindow {
                     required property string displayName
                     required property bool fromAccountData
                     required property bool fromCurrentRoom
+                    required property string statekey
 
                     title: displayName
                     subtitle: {
@@ -112,6 +113,7 @@ ApplicationWindow {
                             return qsTr("Globally enabled pack");
                     }
                     selectedIndex: currentPackIndex
+                    roomid: statekey
 
                     TapHandler {
                         onSingleTapped: currentPackIndex = index
@@ -135,6 +137,7 @@ ApplicationWindow {
                     property string packName: currentPack ? currentPack.packname : ""
                     property string attribution: currentPack ? currentPack.attribution : ""
                     property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
+                    property string statekey: currentPack ? currentPack.statekey : ""
 
                     anchors.fill: parent
                     anchors.margins: Nheko.paddingLarge
@@ -143,6 +146,7 @@ ApplicationWindow {
                     Avatar {
                         url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/")
                         displayName: packinfo.packName
+                        roomid: packinfo.statekey
                         height: 100
                         width: 100
                         Layout.alignment: Qt.AlignHCenter
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
index e0f1785170d9ec131e6a654ba9657769c0c3a59a..12211c600fe055e4c7a42208f35d60f09ada6071 100644
--- a/resources/qml/dialogs/InputDialog.qml
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -12,6 +12,7 @@ ApplicationWindow {
     id: inputDialog
 
     property alias prompt: promptLabel.text
+    property alias echoMode: statusInput.echoMode
     property var onAccepted: undefined
 
     modality: Qt.NonModal
@@ -21,7 +22,8 @@ ApplicationWindow {
     height: fontMetrics.lineSpacing * 7
 
     ColumnLayout {
-        anchors.margins: Nheko.paddingLarge
+        spacing: Nheko.paddingMedium
+        anchors.margins: Nheko.paddingMedium
         anchors.fill: parent
 
         Label {
diff --git a/resources/qml/InviteDialog.qml b/resources/qml/dialogs/InviteDialog.qml
similarity index 98%
rename from resources/qml/InviteDialog.qml
rename to resources/qml/dialogs/InviteDialog.qml
index 2c0e15a7f6a1a52bf9cb5f268162917f055d141b..86c176be2006f5d6300be1919ad1268454a4cad2 100644
--- a/resources/qml/InviteDialog.qml
+++ b/resources/qml/dialogs/InviteDialog.qml
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+import ".."
 import QtQuick 2.12
 import QtQuick.Controls 2.12
 import QtQuick.Layouts 1.12
@@ -34,7 +35,7 @@ ApplicationWindow {
     width: 340
     palette: Nheko.colors
     color: Nheko.colors.window
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     Component.onCompleted: Nheko.reparent(inviteDialogRoot)
 
     Shortcut {
diff --git a/resources/qml/dialogs/JoinRoomDialog.qml b/resources/qml/dialogs/JoinRoomDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..df31d9949ee33309620f1ef8a8d7c1d9947a624d
--- /dev/null
+++ b/resources/qml/dialogs/JoinRoomDialog.qml
@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick 2.12
+import QtQuick.Controls 2.5
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: joinRoomRoot
+
+    title: qsTr("Join room")
+    modality: Qt.WindowModal
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+    palette: Nheko.colors
+    color: Nheko.colors.window
+    Component.onCompleted: Nheko.reparent(joinRoomRoot)
+    width: 350
+    height: fontMetrics.lineSpacing * 7
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: dbb.rejected()
+    }
+
+    ColumnLayout {
+        spacing: Nheko.paddingMedium
+        anchors.margins: Nheko.paddingMedium
+        anchors.fill: parent
+
+        Label {
+            id: promptLabel
+
+            text: qsTr("Room ID or alias")
+            color: Nheko.colors.text
+        }
+
+        MatrixTextField {
+            id: input
+
+            focus: true
+            Layout.fillWidth: true
+            onAccepted: {
+                if (input.text.match("#.+?:.{3,}"))
+                    dbb.accepted();
+
+            }
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        id: dbb
+
+        onAccepted: {
+            Nheko.joinRoom(input.text);
+            joinRoomRoot.close();
+        }
+        onRejected: {
+            joinRoomRoot.close();
+        }
+
+        Button {
+            text: "Join"
+            enabled: input.text.match("#.+?:.{3,}")
+            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+        }
+
+        Button {
+            text: "Cancel"
+            DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
+        }
+
+    }
+
+}
diff --git a/resources/qml/dialogs/LeaveRoomDialog.qml b/resources/qml/dialogs/LeaveRoomDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..e9c12e8f1b5556629a34c1750cff6dece03ccf51
--- /dev/null
+++ b/resources/qml/dialogs/LeaveRoomDialog.qml
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Qt.labs.platform 1.1
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+MessageDialog {
+    id: leaveRoomRoot
+
+    required property string roomId
+
+    title: qsTr("Leave room")
+    text: qsTr("Are you sure you want to leave?")
+    modality: Qt.ApplicationModal
+    buttons: Dialog.Ok | Dialog.Cancel
+    onAccepted: Rooms.leave(roomId)
+}
diff --git a/resources/qml/dialogs/LogoutDialog.qml b/resources/qml/dialogs/LogoutDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..eb82dd158c93206cf0335dedd9580e8984c80530
--- /dev/null
+++ b/resources/qml/dialogs/LogoutDialog.qml
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Qt.labs.platform 1.1
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+MessageDialog {
+    id: logoutRoot
+
+    title: qsTr("Log out")
+    text: CallManager.isOnCall ? qsTr("A call is in progress. Log out?") : qsTr("Are you sure you want to log out?")
+    modality: Qt.WindowModal
+    flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+    buttons: Dialog.Ok | Dialog.Cancel
+    onAccepted: Nheko.logout()
+}
diff --git a/resources/qml/dialogs/PhoneNumberInputDialog.qml b/resources/qml/dialogs/PhoneNumberInputDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..b4f2a9f026df0ca17507581adb8100a9f9ed920c
--- /dev/null
+++ b/resources/qml/dialogs/PhoneNumberInputDialog.qml
@@ -0,0 +1,1744 @@
+// SPDX-FileCopyrightText: 2021 Mirian Margiani
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick 2.12
+import QtQuick.Controls 2.5
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: inputDialog
+
+    property alias prompt: promptLabel.text
+    property alias echoMode: statusInput.echoMode
+    property var onAccepted: undefined
+
+    modality: Qt.NonModal
+    flags: Qt.Dialog
+    Component.onCompleted: Nheko.reparent(inputDialog)
+    width: 350
+    height: fontMetrics.lineSpacing * 7
+
+    GridLayout {
+        rowSpacing: Nheko.paddingMedium
+        columnSpacing: Nheko.paddingMedium
+        anchors.margins: Nheko.paddingMedium
+        anchors.fill: parent
+        columns: 2
+
+        Label {
+            id: promptLabel
+
+            Layout.columnSpan: 2
+            color: Nheko.colors.text
+        }
+
+        ComboBox {
+            id: numberPrefix
+
+            editable: false
+
+            delegate: ItemDelegate {
+                text: n + " (" + p + ")"
+            }
+            // taken from https://gitlab.com/whisperfish/whisperfish/-/blob/master/qml/js/countries.js
+
+            //n=name,i=ISO,p=prefix -- see countries.js.md for source
+            model: ListModel {
+                ListElement {
+                    n: "Afghanistan"
+                    i: "AF"
+                    p: "+93"
+                }
+
+                ListElement {
+                    n: "Ã…land Islands"
+                    i: "AX"
+                    p: "+358 18"
+                }
+
+                ListElement {
+                    n: "Albania"
+                    i: "AL"
+                    p: "+355"
+                }
+
+                ListElement {
+                    n: "Algeria"
+                    i: "DZ"
+                    p: "+213"
+                }
+
+                ListElement {
+                    n: "American Samoa"
+                    i: "AS"
+                    p: "+1 684"
+                }
+
+                ListElement {
+                    n: "Andorra"
+                    i: "AD"
+                    p: "+376"
+                }
+
+                ListElement {
+                    n: "Angola"
+                    i: "AO"
+                    p: "+244"
+                }
+
+                ListElement {
+                    n: "Anguilla"
+                    i: "AI"
+                    p: "+1 264"
+                }
+
+                ListElement {
+                    n: "Antigua and Barbuda"
+                    i: "AG"
+                    p: "+1 268"
+                }
+
+                ListElement {
+                    n: "Argentina"
+                    i: "AR"
+                    p: "+54"
+                }
+
+                ListElement {
+                    n: "Armenia"
+                    i: "AM"
+                    p: "+374"
+                }
+
+                ListElement {
+                    n: "Aruba"
+                    i: "AW"
+                    p: "+297"
+                }
+
+                ListElement {
+                    n: "Ascension"
+                    i: "SH"
+                    p: "+247"
+                }
+
+                ListElement {
+                    n: "Australia"
+                    i: "AU"
+                    p: "+61"
+                }
+
+                ListElement {
+                    n: "Australian Antarctic Territory"
+                    i: "AQ"
+                    p: "+672 1"
+                }
+                //ListElement{n:"Australian External Territories";i:"";p:"+672"} // NO ISO
+
+                ListElement {
+                    n: "Austria"
+                    i: "AT"
+                    p: "+43"
+                }
+
+                ListElement {
+                    n: "Azerbaijan"
+                    i: "AZ"
+                    p: "+994"
+                }
+
+                ListElement {
+                    n: "Bahamas"
+                    i: "BS"
+                    p: "+1 242"
+                }
+
+                ListElement {
+                    n: "Bahrain"
+                    i: "BH"
+                    p: "+973"
+                }
+
+                ListElement {
+                    n: "Bangladesh"
+                    i: "BD"
+                    p: "+880"
+                }
+
+                ListElement {
+                    n: "Barbados"
+                    i: "BB"
+                    p: "+1 246"
+                }
+
+                ListElement {
+                    n: "Barbuda"
+                    i: "AG"
+                    p: "+1 268"
+                }
+
+                ListElement {
+                    n: "Belarus"
+                    i: "BY"
+                    p: "+375"
+                }
+
+                ListElement {
+                    n: "Belgium"
+                    i: "BE"
+                    p: "+32"
+                }
+
+                ListElement {
+                    n: "Belize"
+                    i: "BZ"
+                    p: "+501"
+                }
+
+                ListElement {
+                    n: "Benin"
+                    i: "BJ"
+                    p: "+229"
+                }
+
+                ListElement {
+                    n: "Bermuda"
+                    i: "BM"
+                    p: "+1 441"
+                }
+
+                ListElement {
+                    n: "Bhutan"
+                    i: "BT"
+                    p: "+975"
+                }
+
+                ListElement {
+                    n: "Bolivia"
+                    i: "BO"
+                    p: "+591"
+                }
+
+                ListElement {
+                    n: "Bonaire"
+                    i: "BQ"
+                    p: "+599 7"
+                }
+
+                ListElement {
+                    n: "Bosnia and Herzegovina"
+                    i: "BA"
+                    p: "+387"
+                }
+
+                ListElement {
+                    n: "Botswana"
+                    i: "BW"
+                    p: "+267"
+                }
+
+                ListElement {
+                    n: "Brazil"
+                    i: "BR"
+                    p: "+55"
+                }
+
+                ListElement {
+                    n: "British Indian Ocean Territory"
+                    i: "IO"
+                    p: "+246"
+                }
+
+                ListElement {
+                    n: "Brunei Darussalam"
+                    i: "BN"
+                    p: "+673"
+                }
+
+                ListElement {
+                    n: "Bulgaria"
+                    i: "BG"
+                    p: "+359"
+                }
+
+                ListElement {
+                    n: "Burkina Faso"
+                    i: "BF"
+                    p: "+226"
+                }
+
+                ListElement {
+                    n: "Burundi"
+                    i: "BI"
+                    p: "+257"
+                }
+
+                ListElement {
+                    n: "Cambodia"
+                    i: "KH"
+                    p: "+855"
+                }
+
+                ListElement {
+                    n: "Cameroon"
+                    i: "CM"
+                    p: "+237"
+                }
+
+                ListElement {
+                    n: "Canada"
+                    i: "CA"
+                    p: "+1"
+                }
+
+                ListElement {
+                    n: "Cape Verde"
+                    i: "CV"
+                    p: "+238"
+                }
+                //ListElement{n:"Caribbean Netherlands";i:"";p:"+599 3"} // NO ISO
+
+                //ListElement{n:"Caribbean Netherlands";i:"";p:"+599 4"} // NO ISO
+                //ListElement{n:"Caribbean Netherlands";i:"";p:"+599 7"} // NO ISO
+                ListElement {
+                    n: "Cayman Islands"
+                    i: "KY"
+                    p: "+1 345"
+                }
+
+                ListElement {
+                    n: "Central African Republic"
+                    i: "CF"
+                    p: "+236"
+                }
+
+                ListElement {
+                    n: "Chad"
+                    i: "TD"
+                    p: "+235"
+                }
+
+                ListElement {
+                    n: "Chatham Island (New Zealand)"
+                    i: "NZ"
+                    p: "+64"
+                }
+
+                ListElement {
+                    n: "Chile"
+                    i: "CL"
+                    p: "+56"
+                }
+
+                ListElement {
+                    n: "China"
+                    i: "CN"
+                    p: "+86"
+                }
+
+                ListElement {
+                    n: "Christmas Island"
+                    i: "CX"
+                    p: "+61 89164"
+                }
+
+                ListElement {
+                    n: "Cocos (Keeling) Islands"
+                    i: "CC"
+                    p: "+61 89162"
+                }
+
+                ListElement {
+                    n: "Colombia"
+                    i: "CO"
+                    p: "+57"
+                }
+
+                ListElement {
+                    n: "Comoros"
+                    i: "KM"
+                    p: "+269"
+                }
+
+                ListElement {
+                    n: "Congo (Democratic Republic of the)"
+                    i: "CD"
+                    p: "+243"
+                }
+
+                ListElement {
+                    n: "Congo"
+                    i: "CG"
+                    p: "+242"
+                }
+
+                ListElement {
+                    n: "Cook Islands"
+                    i: "CK"
+                    p: "+682"
+                }
+
+                ListElement {
+                    n: "Costa Rica"
+                    i: "CR"
+                    p: "+506"
+                }
+
+                ListElement {
+                    n: "Côte d'Ivoire"
+                    i: "CI"
+                    p: "+225"
+                }
+
+                ListElement {
+                    n: "Croatia"
+                    i: "HR"
+                    p: "+385"
+                }
+
+                ListElement {
+                    n: "Cuba"
+                    i: "CU"
+                    p: "+53"
+                }
+
+                ListElement {
+                    n: "Curaçao"
+                    i: "CW"
+                    p: "+599 9"
+                }
+
+                ListElement {
+                    n: "Cyprus"
+                    i: "CY"
+                    p: "+357"
+                }
+
+                ListElement {
+                    n: "Czech Republic"
+                    i: "CZ"
+                    p: "+420"
+                }
+
+                ListElement {
+                    n: "Denmark"
+                    i: "DK"
+                    p: "+45"
+                }
+                //ListElement{n:"Diego Garcia";i:"";p:"+246"} // NO ISO, OCC. BY GB
+
+                ListElement {
+                    n: "Djibouti"
+                    i: "DJ"
+                    p: "+253"
+                }
+
+                ListElement {
+                    n: "Dominica"
+                    i: "DM"
+                    p: "+1 767"
+                }
+
+                ListElement {
+                    n: "Dominican Republic"
+                    i: "DO"
+                    p: "+1 809"
+                }
+
+                ListElement {
+                    n: "Dominican Republic"
+                    i: "DO"
+                    p: "+1 829"
+                }
+
+                ListElement {
+                    n: "Dominican Republic"
+                    i: "DO"
+                    p: "+1 849"
+                }
+
+                ListElement {
+                    n: "Easter Island"
+                    i: "CL"
+                    p: "+56"
+                }
+
+                ListElement {
+                    n: "Ecuador"
+                    i: "EC"
+                    p: "+593"
+                }
+
+                ListElement {
+                    n: "Egypt"
+                    i: "EG"
+                    p: "+20"
+                }
+
+                ListElement {
+                    n: "El Salvador"
+                    i: "SV"
+                    p: "+503"
+                }
+
+                ListElement {
+                    n: "Equatorial Guinea"
+                    i: "GQ"
+                    p: "+240"
+                }
+
+                ListElement {
+                    n: "Eritrea"
+                    i: "ER"
+                    p: "+291"
+                }
+
+                ListElement {
+                    n: "Estonia"
+                    i: "EE"
+                    p: "+372"
+                }
+
+                ListElement {
+                    n: "eSwatini"
+                    i: "SZ"
+                    p: "+268"
+                }
+
+                ListElement {
+                    n: "Ethiopia"
+                    i: "ET"
+                    p: "+251"
+                }
+
+                ListElement {
+                    n: "Falkland Islands (Malvinas)"
+                    i: "FK"
+                    p: "+500"
+                }
+
+                ListElement {
+                    n: "Faroe Islands"
+                    i: "FO"
+                    p: "+298"
+                }
+
+                ListElement {
+                    n: "Fiji"
+                    i: "FJ"
+                    p: "+679"
+                }
+
+                ListElement {
+                    n: "Finland"
+                    i: "FI"
+                    p: "+358"
+                }
+
+                ListElement {
+                    n: "France"
+                    i: "FR"
+                    p: "+33"
+                }
+                //ListElement{n:"French Antilles";i:"";p:"+596"} // NO ISO
+
+                ListElement {
+                    n: "French Guiana"
+                    i: "GF"
+                    p: "+594"
+                }
+
+                ListElement {
+                    n: "French Polynesia"
+                    i: "PF"
+                    p: "+689"
+                }
+
+                ListElement {
+                    n: "Gabon"
+                    i: "GA"
+                    p: "+241"
+                }
+
+                ListElement {
+                    n: "Gambia"
+                    i: "GM"
+                    p: "+220"
+                }
+
+                ListElement {
+                    n: "Georgia"
+                    i: "GE"
+                    p: "+995"
+                }
+
+                ListElement {
+                    n: "Germany"
+                    i: "DE"
+                    p: "+49"
+                }
+
+                ListElement {
+                    n: "Ghana"
+                    i: "GH"
+                    p: "+233"
+                }
+
+                ListElement {
+                    n: "Gibraltar"
+                    i: "GI"
+                    p: "+350"
+                }
+
+                ListElement {
+                    n: "Greece"
+                    i: "GR"
+                    p: "+30"
+                }
+
+                ListElement {
+                    n: "Greenland"
+                    i: "GL"
+                    p: "+299"
+                }
+
+                ListElement {
+                    n: "Grenada"
+                    i: "GD"
+                    p: "+1 473"
+                }
+
+                ListElement {
+                    n: "Guadeloupe"
+                    i: "GP"
+                    p: "+590"
+                }
+
+                ListElement {
+                    n: "Guam"
+                    i: "GU"
+                    p: "+1 671"
+                }
+
+                ListElement {
+                    n: "Guatemala"
+                    i: "GT"
+                    p: "+502"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 1481"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 7781"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 7839"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 7911"
+                }
+
+                ListElement {
+                    n: "Guinea-Bissau"
+                    i: "GW"
+                    p: "+245"
+                }
+
+                ListElement {
+                    n: "Guinea"
+                    i: "GN"
+                    p: "+224"
+                }
+
+                ListElement {
+                    n: "Guyana"
+                    i: "GY"
+                    p: "+592"
+                }
+
+                ListElement {
+                    n: "Haiti"
+                    i: "HT"
+                    p: "+509"
+                }
+
+                ListElement {
+                    n: "Honduras"
+                    i: "HN"
+                    p: "+504"
+                }
+
+                ListElement {
+                    n: "Hong Kong"
+                    i: "HK"
+                    p: "+852"
+                }
+
+                ListElement {
+                    n: "Hungary"
+                    i: "HU"
+                    p: "+36"
+                }
+
+                ListElement {
+                    n: "Iceland"
+                    i: "IS"
+                    p: "+354"
+                }
+
+                ListElement {
+                    n: "India"
+                    i: "IN"
+                    p: "+91"
+                }
+
+                ListElement {
+                    n: "Indonesia"
+                    i: "ID"
+                    p: "+62"
+                }
+
+                ListElement {
+                    n: "Iran"
+                    i: "IR"
+                    p: "+98"
+                }
+
+                ListElement {
+                    n: "Iraq"
+                    i: "IQ"
+                    p: "+964"
+                }
+
+                ListElement {
+                    n: "Ireland"
+                    i: "IE"
+                    p: "+353"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 1624"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 7524"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 7624"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 7924"
+                }
+
+                ListElement {
+                    n: "Israel"
+                    i: "IL"
+                    p: "+972"
+                }
+
+                ListElement {
+                    n: "Italy"
+                    i: "IT"
+                    p: "+39"
+                }
+
+                ListElement {
+                    n: "Jamaica"
+                    i: "JM"
+                    p: "+1 876"
+                }
+
+                ListElement {
+                    n: "Jan Mayen"
+                    i: "SJ"
+                    p: "+47 79"
+                }
+
+                ListElement {
+                    n: "Japan"
+                    i: "JP"
+                    p: "+81"
+                }
+
+                ListElement {
+                    n: "Jersey"
+                    i: "JE"
+                    p: "+44 1534"
+                }
+
+                ListElement {
+                    n: "Jordan"
+                    i: "JO"
+                    p: "+962"
+                }
+
+                ListElement {
+                    n: "Kazakhstan"
+                    i: "KZ"
+                    p: "+7 6"
+                }
+
+                ListElement {
+                    n: "Kazakhstan"
+                    i: "KZ"
+                    p: "+7 7"
+                }
+
+                ListElement {
+                    n: "Kenya"
+                    i: "KE"
+                    p: "+254"
+                }
+
+                ListElement {
+                    n: "Kiribati"
+                    i: "KI"
+                    p: "+686"
+                }
+
+                ListElement {
+                    n: "Korea (North)"
+                    i: "KP"
+                    p: "+850"
+                }
+
+                ListElement {
+                    n: "Korea (South)"
+                    i: "KR"
+                    p: "+82"
+                }
+                // TEMP. CODE
+
+                ListElement {
+                    n: "Kosovo"
+                    i: "XK"
+                    p: "+383"
+                }
+
+                ListElement {
+                    n: "Kuwait"
+                    i: "KW"
+                    p: "+965"
+                }
+
+                ListElement {
+                    n: "Kyrgyzstan"
+                    i: "KG"
+                    p: "+996"
+                }
+
+                ListElement {
+                    n: "Laos"
+                    i: "LA"
+                    p: "+856"
+                }
+
+                ListElement {
+                    n: "Latvia"
+                    i: "LV"
+                    p: "+371"
+                }
+
+                ListElement {
+                    n: "Lebanon"
+                    i: "LB"
+                    p: "+961"
+                }
+
+                ListElement {
+                    n: "Lesotho"
+                    i: "LS"
+                    p: "+266"
+                }
+
+                ListElement {
+                    n: "Liberia"
+                    i: "LR"
+                    p: "+231"
+                }
+
+                ListElement {
+                    n: "Libya"
+                    i: "LY"
+                    p: "+218"
+                }
+
+                ListElement {
+                    n: "Liechtenstein"
+                    i: "LI"
+                    p: "+423"
+                }
+
+                ListElement {
+                    n: "Lithuania"
+                    i: "LT"
+                    p: "+370"
+                }
+
+                ListElement {
+                    n: "Luxembourg"
+                    i: "LU"
+                    p: "+352"
+                }
+
+                ListElement {
+                    n: "Macau (Macao)"
+                    i: "MO"
+                    p: "+853"
+                }
+
+                ListElement {
+                    n: "Madagascar"
+                    i: "MG"
+                    p: "+261"
+                }
+
+                ListElement {
+                    n: "Malawi"
+                    i: "MW"
+                    p: "+265"
+                }
+
+                ListElement {
+                    n: "Malaysia"
+                    i: "MY"
+                    p: "+60"
+                }
+
+                ListElement {
+                    n: "Maldives"
+                    i: "MV"
+                    p: "+960"
+                }
+
+                ListElement {
+                    n: "Mali"
+                    i: "ML"
+                    p: "+223"
+                }
+
+                ListElement {
+                    n: "Malta"
+                    i: "MT"
+                    p: "+356"
+                }
+
+                ListElement {
+                    n: "Marshall Islands"
+                    i: "MH"
+                    p: "+692"
+                }
+
+                ListElement {
+                    n: "Martinique"
+                    i: "MQ"
+                    p: "+596"
+                }
+
+                ListElement {
+                    n: "Mauritania"
+                    i: "MR"
+                    p: "+222"
+                }
+
+                ListElement {
+                    n: "Mauritius"
+                    i: "MU"
+                    p: "+230"
+                }
+
+                ListElement {
+                    n: "Mayotte"
+                    i: "YT"
+                    p: "+262 269"
+                }
+
+                ListElement {
+                    n: "Mayotte"
+                    i: "YT"
+                    p: "+262 639"
+                }
+
+                ListElement {
+                    n: "Mexico"
+                    i: "MX"
+                    p: "+52"
+                }
+
+                ListElement {
+                    n: "Micronesia (Federated States of)"
+                    i: "FM"
+                    p: "+691"
+                }
+
+                ListElement {
+                    n: "Midway Island (USA)"
+                    i: "US"
+                    p: "+1 808"
+                }
+
+                ListElement {
+                    n: "Moldova"
+                    i: "MD"
+                    p: "+373"
+                }
+
+                ListElement {
+                    n: "Monaco"
+                    i: "MC"
+                    p: "+377"
+                }
+
+                ListElement {
+                    n: "Mongolia"
+                    i: "MN"
+                    p: "+976"
+                }
+
+                ListElement {
+                    n: "Montenegro"
+                    i: "ME"
+                    p: "+382"
+                }
+
+                ListElement {
+                    n: "Montserrat"
+                    i: "MS"
+                    p: "+1 664"
+                }
+
+                ListElement {
+                    n: "Morocco"
+                    i: "MA"
+                    p: "+212"
+                }
+
+                ListElement {
+                    n: "Mozambique"
+                    i: "MZ"
+                    p: "+258"
+                }
+
+                ListElement {
+                    n: "Myanmar"
+                    i: "MM"
+                    p: "+95"
+                }
+                // NO OWN ISO, DISPUTED
+
+                ListElement {
+                    n: "Nagorno-Karabakh"
+                    i: "AZ"
+                    p: "+374 47"
+                }
+                // NO OWN ISO, DISPUTED
+
+                ListElement {
+                    n: "Nagorno-Karabakh"
+                    i: "AZ"
+                    p: "+374 97"
+                }
+
+                ListElement {
+                    n: "Namibia"
+                    i: "NA"
+                    p: "+264"
+                }
+
+                ListElement {
+                    n: "Nauru"
+                    i: "NR"
+                    p: "+674"
+                }
+
+                ListElement {
+                    n: "Nepal"
+                    i: "NP"
+                    p: "+977"
+                }
+
+                ListElement {
+                    n: "Netherlands"
+                    i: "NL"
+                    p: "+31"
+                }
+
+                ListElement {
+                    n: "Nevis"
+                    i: "KN"
+                    p: "+1 869"
+                }
+
+                ListElement {
+                    n: "New Caledonia"
+                    i: "NC"
+                    p: "+687"
+                }
+
+                ListElement {
+                    n: "New Zealand"
+                    i: "NZ"
+                    p: "+64"
+                }
+
+                ListElement {
+                    n: "Nicaragua"
+                    i: "NI"
+                    p: "+505"
+                }
+
+                ListElement {
+                    n: "Nigeria"
+                    i: "NG"
+                    p: "+234"
+                }
+
+                ListElement {
+                    n: "Niger"
+                    i: "NE"
+                    p: "+227"
+                }
+
+                ListElement {
+                    n: "Niue"
+                    i: "NU"
+                    p: "+683"
+                }
+
+                ListElement {
+                    n: "Norfolk Island"
+                    i: "NF"
+                    p: "+672 3"
+                }
+                // OCC. BY TR
+
+                ListElement {
+                    n: "Northern Cyprus"
+                    i: "CY"
+                    p: "+90 392"
+                }
+
+                ListElement {
+                    n: "Northern Ireland"
+                    i: "GB"
+                    p: "+44 28"
+                }
+
+                ListElement {
+                    n: "Northern Mariana Islands"
+                    i: "MP"
+                    p: "+1 670"
+                }
+
+                ListElement {
+                    n: "North Macedonia"
+                    i: "MK"
+                    p: "+389"
+                }
+
+                ListElement {
+                    n: "Norway"
+                    i: "NO"
+                    p: "+47"
+                }
+
+                ListElement {
+                    n: "Oman"
+                    i: "OM"
+                    p: "+968"
+                }
+
+                ListElement {
+                    n: "Pakistan"
+                    i: "PK"
+                    p: "+92"
+                }
+
+                ListElement {
+                    n: "Palau"
+                    i: "PW"
+                    p: "+680"
+                }
+
+                ListElement {
+                    n: "Palestine (State of)"
+                    i: "PS"
+                    p: "+970"
+                }
+
+                ListElement {
+                    n: "Panama"
+                    i: "PA"
+                    p: "+507"
+                }
+
+                ListElement {
+                    n: "Papua New Guinea"
+                    i: "PG"
+                    p: "+675"
+                }
+
+                ListElement {
+                    n: "Paraguay"
+                    i: "PY"
+                    p: "+595"
+                }
+
+                ListElement {
+                    n: "Peru"
+                    i: "PE"
+                    p: "+51"
+                }
+
+                ListElement {
+                    n: "Philippines"
+                    i: "PH"
+                    p: "+63"
+                }
+
+                ListElement {
+                    n: "Pitcairn Islands"
+                    i: "PN"
+                    p: "+64"
+                }
+
+                ListElement {
+                    n: "Poland"
+                    i: "PL"
+                    p: "+48"
+                }
+
+                ListElement {
+                    n: "Portugal"
+                    i: "PT"
+                    p: "+351"
+                }
+
+                ListElement {
+                    n: "Puerto Rico"
+                    i: "PR"
+                    p: "+1 787"
+                }
+
+                ListElement {
+                    n: "Puerto Rico"
+                    i: "PR"
+                    p: "+1 939"
+                }
+
+                ListElement {
+                    n: "Qatar"
+                    i: "QA"
+                    p: "+974"
+                }
+
+                ListElement {
+                    n: "Réunion"
+                    i: "RE"
+                    p: "+262"
+                }
+
+                ListElement {
+                    n: "Romania"
+                    i: "RO"
+                    p: "+40"
+                }
+
+                ListElement {
+                    n: "Russia"
+                    i: "RU"
+                    p: "+7"
+                }
+
+                ListElement {
+                    n: "Rwanda"
+                    i: "RW"
+                    p: "+250"
+                }
+
+                ListElement {
+                    n: "Saba"
+                    i: "BQ"
+                    p: "+599 4"
+                }
+
+                ListElement {
+                    n: "Saint Barthélemy"
+                    i: "BL"
+                    p: "+590"
+                }
+
+                ListElement {
+                    n: "Saint Helena"
+                    i: "SH"
+                    p: "+290"
+                }
+
+                ListElement {
+                    n: "Saint Kitts and Nevis"
+                    i: "KN"
+                    p: "+1 869"
+                }
+
+                ListElement {
+                    n: "Saint Lucia"
+                    i: "LC"
+                    p: "+1 758"
+                }
+
+                ListElement {
+                    n: "Saint Martin (France)"
+                    i: "MF"
+                    p: "+590"
+                }
+
+                ListElement {
+                    n: "Saint Pierre and Miquelon"
+                    i: "PM"
+                    p: "+508"
+                }
+
+                ListElement {
+                    n: "Saint Vincent and the Grenadines"
+                    i: "VC"
+                    p: "+1 784"
+                }
+
+                ListElement {
+                    n: "Samoa"
+                    i: "WS"
+                    p: "+685"
+                }
+
+                ListElement {
+                    n: "San Marino"
+                    i: "SM"
+                    p: "+378"
+                }
+
+                ListElement {
+                    n: "São Tomé and Príncipe"
+                    i: "ST"
+                    p: "+239"
+                }
+
+                ListElement {
+                    n: "Saudi Arabia"
+                    i: "SA"
+                    p: "+966"
+                }
+
+                ListElement {
+                    n: "Senegal"
+                    i: "SN"
+                    p: "+221"
+                }
+
+                ListElement {
+                    n: "Serbia"
+                    i: "RS"
+                    p: "+381"
+                }
+
+                ListElement {
+                    n: "Seychelles"
+                    i: "SC"
+                    p: "+248"
+                }
+
+                ListElement {
+                    n: "Sierra Leone"
+                    i: "SL"
+                    p: "+232"
+                }
+
+                ListElement {
+                    n: "Singapore"
+                    i: "SG"
+                    p: "+65"
+                }
+
+                ListElement {
+                    n: "Sint Eustatius"
+                    i: "BQ"
+                    p: "+599 3"
+                }
+
+                ListElement {
+                    n: "Sint Maarten (Netherlands)"
+                    i: "SX"
+                    p: "+1 721"
+                }
+
+                ListElement {
+                    n: "Slovakia"
+                    i: "SK"
+                    p: "+421"
+                }
+
+                ListElement {
+                    n: "Slovenia"
+                    i: "SI"
+                    p: "+386"
+                }
+
+                ListElement {
+                    n: "Solomon Islands"
+                    i: "SB"
+                    p: "+677"
+                }
+
+                ListElement {
+                    n: "Somalia"
+                    i: "SO"
+                    p: "+252"
+                }
+
+                ListElement {
+                    n: "South Africa"
+                    i: "ZA"
+                    p: "+27"
+                }
+
+                ListElement {
+                    n: "South Georgia and the South Sandwich Islands"
+                    i: "GS"
+                    p: "+500"
+                }
+                // NO OWN ISO, DISPUTED
+
+                ListElement {
+                    n: "South Ossetia"
+                    i: "GE"
+                    p: "+995 34"
+                }
+
+                ListElement {
+                    n: "South Sudan"
+                    i: "SS"
+                    p: "+211"
+                }
+
+                ListElement {
+                    n: "Spain"
+                    i: "ES"
+                    p: "+34"
+                }
+
+                ListElement {
+                    n: "Sri Lanka"
+                    i: "LK"
+                    p: "+94"
+                }
+
+                ListElement {
+                    n: "Sudan"
+                    i: "SD"
+                    p: "+249"
+                }
+
+                ListElement {
+                    n: "Suriname"
+                    i: "SR"
+                    p: "+597"
+                }
+
+                ListElement {
+                    n: "Svalbard"
+                    i: "SJ"
+                    p: "+47 79"
+                }
+
+                ListElement {
+                    n: "Sweden"
+                    i: "SE"
+                    p: "+46"
+                }
+
+                ListElement {
+                    n: "Switzerland"
+                    i: "CH"
+                    p: "+41"
+                }
+
+                ListElement {
+                    n: "Syria"
+                    i: "SY"
+                    p: "+963"
+                }
+
+                ListElement {
+                    n: "Taiwan"
+                    i: "SJ"
+                    p: "+886"
+                }
+
+                ListElement {
+                    n: "Tajikistan"
+                    i: "TJ"
+                    p: "+992"
+                }
+
+                ListElement {
+                    n: "Tanzania"
+                    i: "TZ"
+                    p: "+255"
+                }
+
+                ListElement {
+                    n: "Thailand"
+                    i: "TH"
+                    p: "+66"
+                }
+
+                ListElement {
+                    n: "Timor-Leste"
+                    i: "TL"
+                    p: "+670"
+                }
+
+                ListElement {
+                    n: "Togo"
+                    i: "TG"
+                    p: "+228"
+                }
+
+                ListElement {
+                    n: "Tokelau"
+                    i: "TK"
+                    p: "+690"
+                }
+
+                ListElement {
+                    n: "Tonga"
+                    i: "TO"
+                    p: "+676"
+                }
+
+                ListElement {
+                    n: "Transnistria"
+                    i: "MD"
+                    p: "+373 2"
+                }
+
+                ListElement {
+                    n: "Transnistria"
+                    i: "MD"
+                    p: "+373 5"
+                }
+
+                ListElement {
+                    n: "Trinidad and Tobago"
+                    i: "TT"
+                    p: "+1 868"
+                }
+
+                ListElement {
+                    n: "Tristan da Cunha"
+                    i: "SH"
+                    p: "+290 8"
+                }
+
+                ListElement {
+                    n: "Tunisia"
+                    i: "TN"
+                    p: "+216"
+                }
+
+                ListElement {
+                    n: "Turkey"
+                    i: "TR"
+                    p: "+90"
+                }
+
+                ListElement {
+                    n: "Turkmenistan"
+                    i: "TM"
+                    p: "+993"
+                }
+
+                ListElement {
+                    n: "Turks and Caicos Islands"
+                    i: "TC"
+                    p: "+1 649"
+                }
+
+                ListElement {
+                    n: "Tuvalu"
+                    i: "TV"
+                    p: "+688"
+                }
+
+                ListElement {
+                    n: "Uganda"
+                    i: "UG"
+                    p: "+256"
+                }
+
+                ListElement {
+                    n: "Ukraine"
+                    i: "UA"
+                    p: "+380"
+                }
+
+                ListElement {
+                    n: "United Arab Emirates"
+                    i: "AE"
+                    p: "+971"
+                }
+
+                ListElement {
+                    n: "United Kingdom"
+                    i: "GB"
+                    p: "+44"
+                }
+
+                ListElement {
+                    n: "United States"
+                    i: "US"
+                    p: "+1"
+                }
+
+                ListElement {
+                    n: "Uruguay"
+                    i: "UY"
+                    p: "+598"
+                }
+
+                ListElement {
+                    n: "Uzbekistan"
+                    i: "UZ"
+                    p: "+998"
+                }
+
+                ListElement {
+                    n: "Vanuatu"
+                    i: "VU"
+                    p: "+678"
+                }
+
+                ListElement {
+                    n: "Vatican City State (Holy See)"
+                    i: "VA"
+                    p: "+379"
+                }
+
+                ListElement {
+                    n: "Vatican City State (Holy See)"
+                    i: "VA"
+                    p: "+39 06 698"
+                }
+
+                ListElement {
+                    n: "Venezuela"
+                    i: "VE"
+                    p: "+58"
+                }
+
+                ListElement {
+                    n: "Vietnam"
+                    i: "VN"
+                    p: "+84"
+                }
+
+                ListElement {
+                    n: "Virgin Islands (British)"
+                    i: "VG"
+                    p: "+1 284"
+                }
+
+                ListElement {
+                    n: "Virgin Islands (US)"
+                    i: "VI"
+                    p: "+1 340"
+                }
+
+                ListElement {
+                    n: "Wake Island (USA)"
+                    i: "US"
+                    p: "+1 808"
+                }
+
+                ListElement {
+                    n: "Wallis and Futuna"
+                    i: "WF"
+                    p: "+681"
+                }
+
+                ListElement {
+                    n: "Yemen"
+                    i: "YE"
+                    p: "+967"
+                }
+
+                ListElement {
+                    n: "Zambia"
+                    i: "ZM"
+                    p: "+260"
+                }
+                // NO OWN ISO, DISPUTED?
+
+                ListElement {
+                    n: "Zanzibar"
+                    i: "TZ"
+                    p: "+255 24"
+                }
+
+                ListElement {
+                    n: "Zimbabwe"
+                    i: "ZW"
+                    p: "+263"
+                }
+
+            }
+
+        }
+
+        MatrixTextField {
+            id: statusInput
+
+            Layout.fillWidth: true
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+        onAccepted: {
+            if (inputDialog.onAccepted)
+                inputDialog.onAccepted(numberPrefix.model.get(numberPrefix.currentIndex).i, statusInput.text);
+
+            inputDialog.close();
+        }
+        onRejected: {
+            inputDialog.close();
+        }
+    }
+
+}
diff --git a/resources/qml/RawMessageDialog.qml b/resources/qml/dialogs/RawMessageDialog.qml
similarity index 97%
rename from resources/qml/RawMessageDialog.qml
rename to resources/qml/dialogs/RawMessageDialog.qml
index e2a476cdd68a598715ad091ed571effbf54238da..c171de7e0204878498056156bb0a001ed3299516 100644
--- a/resources/qml/RawMessageDialog.qml
+++ b/resources/qml/dialogs/RawMessageDialog.qml
@@ -15,7 +15,7 @@ ApplicationWindow {
     width: 420
     palette: Nheko.colors
     color: Nheko.colors.window
-    flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint
+    flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     Component.onCompleted: Nheko.reparent(rawMessageRoot)
 
     Shortcut {
diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/dialogs/ReadReceipts.qml
similarity index 97%
rename from resources/qml/ReadReceipts.qml
rename to resources/qml/dialogs/ReadReceipts.qml
index 9adbfd5c352515cc673647ee0f4c54d9fbfce9d2..e825dd810f6f7d1d95b6703a245003e8b4ec7797 100644
--- a/resources/qml/ReadReceipts.qml
+++ b/resources/qml/dialogs/ReadReceipts.qml
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+import ".."
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.15
@@ -19,7 +20,7 @@ ApplicationWindow {
     minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium
     palette: Nheko.colors
     color: Nheko.colors.window
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     Component.onCompleted: Nheko.reparent(readReceiptsRoot)
 
     Shortcut {
diff --git a/resources/qml/dialogs/RoomDirectory.qml b/resources/qml/dialogs/RoomDirectory.qml
new file mode 100644
index 0000000000000000000000000000000000000000..bb55b27ca1d51d64aa3532748c09f41faa858ae0
--- /dev/null
+++ b/resources/qml/dialogs/RoomDirectory.qml
@@ -0,0 +1,217 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import "../ui"
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: roomDirectoryWindow
+
+    property RoomDirectoryModel publicRooms
+
+    visible: true
+    minimumWidth: 650
+    minimumHeight: 420
+    palette: Nheko.colors
+    color: Nheko.colors.window
+    modality: Qt.WindowModal
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+    Component.onCompleted: Nheko.reparent(roomDirectoryWindow)
+    title: qsTr("Explore Public Rooms")
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: roomDirectoryWindow.close()
+    }
+
+    ListView {
+        id: roomDirView
+
+        anchors.fill: parent
+        model: publicRooms
+
+        ScrollHelper {
+            flickable: parent
+            anchors.fill: parent
+            enabled: !Settings.mobileMode
+        }
+
+        delegate: Rectangle {
+            id: roomDirDelegate
+
+            property color background: Nheko.colors.window
+            property color importantText: Nheko.colors.text
+            property color unimportantText: Nheko.colors.buttonText
+            property int avatarSize: fontMetrics.lineSpacing * 4
+
+            color: background
+            height: avatarSize + Nheko.paddingLarge
+            width: ListView.view.width
+
+            RowLayout {
+                spacing: Nheko.paddingMedium
+                anchors.fill: parent
+                anchors.margins: Nheko.paddingLarge
+                implicitHeight: textContent.height
+
+                Avatar {
+                    id: roomAvatar
+
+                    Layout.alignment: Qt.AlignVCenter
+                    width: avatarSize
+                    height: avatarSize
+                    url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                    roomid: model.roomid
+                    displayName: model.name
+                }
+
+                ColumnLayout {
+                    id: textContent
+
+                    Layout.alignment: Qt.AlignLeft
+                    width: parent.width - avatar.width
+                    Layout.preferredWidth: parent.width - avatar.width
+                    spacing: Nheko.paddingSmall
+
+                    ElidedLabel {
+                        Layout.alignment: Qt.AlignBottom
+                        color: roomDirDelegate.importantText
+                        elideWidth: textContent.width - numMembersRectangle.width - buttonRectangle.width
+                        font.pixelSize: fontMetrics.font.pixelSize * 1.1
+                        fullText: model.name
+                    }
+
+                    RowLayout {
+                        id: roomDescriptionRow
+
+                        Layout.preferredWidth: parent.width
+                        spacing: Nheko.paddingSmall
+                        Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+                        Layout.preferredHeight: fontMetrics.lineSpacing * 4
+
+                        Label {
+                            id: roomTopic
+
+                            color: roomDirDelegate.unimportantText
+                            Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+                            font.pixelSize: fontMetrics.font.pixelSize
+                            elide: Text.ElideRight
+                            maximumLineCount: 2
+                            Layout.fillWidth: true
+                            text: model.topic
+                            verticalAlignment: Text.AlignVCenter
+                            wrapMode: Text.WordWrap
+                        }
+
+                        Item {
+                            id: numMembersRectangle
+
+                            Layout.margins: Nheko.paddingSmall
+                            width: roomCount.width
+
+                            Label {
+                                id: roomCount
+
+                                color: roomDirDelegate.unimportantText
+                                anchors.centerIn: parent
+                                font.pixelSize: fontMetrics.font.pixelSize
+                                text: model.numMembers.toString()
+                            }
+
+                        }
+
+                        Item {
+                            id: buttonRectangle
+
+                            Layout.margins: Nheko.paddingSmall
+                            width: joinRoomButton.width
+
+                            Button {
+                                id: joinRoomButton
+
+                                visible: model.canJoin
+                                anchors.centerIn: parent
+                                text: "Join"
+                                onClicked: publicRooms.joinRoom(model.index)
+                            }
+
+                        }
+
+                    }
+
+                }
+
+            }
+
+        }
+
+        footer: Item {
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: parent.width
+            visible: !publicRooms.reachedEndOfPagination && publicRooms.loadingMoreRooms
+            // hacky but works
+            height: loadingSpinner.height + 2 * Nheko.paddingLarge
+            anchors.margins: Nheko.paddingLarge
+
+            Spinner {
+                id: loadingSpinner
+
+                anchors.centerIn: parent
+                anchors.margins: Nheko.paddingLarge
+                running: visible
+                foreground: Nheko.colors.mid
+            }
+
+        }
+
+    }
+
+    publicRooms: RoomDirectoryModel {
+    }
+
+    header: RowLayout {
+        id: searchBarLayout
+
+        spacing: Nheko.paddingMedium
+        width: parent.width
+        implicitHeight: roomSearch.height
+
+        MatrixTextField {
+            id: roomSearch
+
+            focus: true
+            Layout.fillWidth: true
+            selectByMouse: true
+            font.pixelSize: fontMetrics.font.pixelSize
+            padding: Nheko.paddingMedium
+            color: Nheko.colors.text
+            placeholderText: qsTr("Search for public rooms")
+            onTextChanged: searchTimer.restart()
+        }
+
+        MatrixTextField {
+            id: chooseServer
+
+            Layout.minimumWidth: 0.3 * header.width
+            Layout.maximumWidth: 0.3 * header.width
+            padding: Nheko.paddingMedium
+            color: Nheko.colors.text
+            placeholderText: qsTr("Choose custom homeserver")
+            onTextChanged: publicRooms.setMatrixServer(text)
+        }
+
+        Timer {
+            id: searchTimer
+
+            interval: 350
+            onTriggered: roomDirView.model.setSearchTerm(roomSearch.text)
+        }
+
+    }
+
+}
diff --git a/resources/qml/RoomMembers.qml b/resources/qml/dialogs/RoomMembers.qml
similarity index 97%
rename from resources/qml/RoomMembers.qml
rename to resources/qml/dialogs/RoomMembers.qml
index 8e44855c9f5339aaaf50bf8ebd602edeca2a82ae..b28062924625c053ef8810b1517fa8fa12002a56 100644
--- a/resources/qml/RoomMembers.qml
+++ b/resources/qml/dialogs/RoomMembers.qml
@@ -2,7 +2,8 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import "./ui"
+import ".."
+import "../ui"
 import QtQuick 2.12
 import QtQuick.Controls 2.12
 import QtQuick.Layouts 1.12
@@ -21,7 +22,7 @@ ApplicationWindow {
     minimumHeight: 420
     palette: Nheko.colors
     color: Nheko.colors.window
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     Component.onCompleted: Nheko.reparent(roomMembersRoot)
 
     Shortcut {
@@ -39,6 +40,7 @@ ApplicationWindow {
 
             width: 130
             height: width
+            roomid: members.roomId
             displayName: members.roomName
             Layout.alignment: Qt.AlignHCenter
             url: members.avatarUrl.replace("mxc://", "image://MxcImage/")
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml
similarity index 93%
rename from resources/qml/RoomSettings.qml
rename to resources/qml/dialogs/RoomSettings.qml
index 491a336fe42b89f1a8b0251f4d5001a773eb06cc..0e7749ce62b1c96974e5ed86e727652ac8e918c8 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/dialogs/RoomSettings.qml
@@ -2,7 +2,8 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import "./ui"
+import ".."
+import "../ui"
 import Qt.labs.platform 1.1 as Platform
 import QtQuick 2.15
 import QtQuick.Controls 2.3
@@ -20,7 +21,7 @@ ApplicationWindow {
     palette: Nheko.colors
     color: Nheko.colors.window
     modality: Qt.NonModal
-    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     Component.onCompleted: Nheko.reparent(roomSettingsDialog)
     title: qsTr("Room Settings")
 
@@ -38,6 +39,7 @@ ApplicationWindow {
 
         Avatar {
             url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
+            roomid: roomSettings.roomId
             displayName: roomSettings.roomName
             height: 130
             width: 130
@@ -186,7 +188,16 @@ ApplicationWindow {
 
             ComboBox {
                 enabled: roomSettings.canChangeJoinRules
-                model: [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")]
+                model: {
+                    let opts = [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")];
+                    if (roomSettings.supportsKnocking)
+                        opts.push(qsTr("By knocking"));
+
+                    if (roomSettings.supportsRestricted)
+                        opts.push(qsTr("Restricted by membership in other rooms"));
+
+                    return opts;
+                }
                 currentIndex: roomSettings.accessJoinRules
                 onActivated: {
                     roomSettings.changeAccessRules(index);
diff --git a/resources/qml/dialogs/UserProfile.qml b/resources/qml/dialogs/UserProfile.qml
new file mode 100644
index 0000000000000000000000000000000000000000..da573ec1f49d3489a808997800988727f9c81233
--- /dev/null
+++ b/resources/qml/dialogs/UserProfile.qml
@@ -0,0 +1,433 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import "../device-verification"
+import "../ui"
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.13
+import im.nheko 1.0
+
+ApplicationWindow {
+    // this does not work in ApplicationWindow, just in Window
+    //transientParent: Nheko.mainwindow()
+
+    id: userProfileDialog
+
+    property var profile
+
+    height: 650
+    width: 420
+    minimumWidth: 150
+    minimumHeight: 150
+    palette: Nheko.colors
+    color: Nheko.colors.window
+    title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
+    modality: Qt.NonModal
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+    Component.onCompleted: Nheko.reparent(userProfileDialog)
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: userProfileDialog.close()
+    }
+
+    ListView {
+        id: devicelist
+
+        Layout.fillHeight: true
+        Layout.fillWidth: true
+        clip: true
+        spacing: 8
+        boundsBehavior: Flickable.StopAtBounds
+        model: profile.deviceList
+        anchors.fill: parent
+        anchors.margins: 10
+        footerPositioning: ListView.OverlayFooter
+
+        ScrollHelper {
+            flickable: parent
+            anchors.fill: parent
+            enabled: !Settings.mobileMode
+        }
+
+        header: ColumnLayout {
+            id: contentL
+
+            width: devicelist.width
+            spacing: 10
+
+            Avatar {
+                id: displayAvatar
+
+                url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
+                height: 130
+                width: 130
+                displayName: profile.displayName
+                userid: profile.userid
+                Layout.alignment: Qt.AlignHCenter
+                onClicked: TimelineManager.openImageOverlay(profile.avatarUrl, "")
+
+                ImageButton {
+                    hoverEnabled: true
+                    ToolTip.visible: hovered
+                    ToolTip.text: profile.isGlobalUserProfile ? qsTr("Change avatar globally.") : qsTr("Change avatar. Will only apply to this room.")
+                    anchors.left: displayAvatar.left
+                    anchors.top: displayAvatar.top
+                    anchors.leftMargin: Nheko.paddingMedium
+                    anchors.topMargin: Nheko.paddingMedium
+                    visible: profile.isSelf
+                    image: ":/icons/icons/ui/edit.png"
+                    onClicked: profile.changeAvatar()
+                }
+
+            }
+
+            Spinner {
+                Layout.alignment: Qt.AlignHCenter
+                running: profile.isLoading
+                visible: profile.isLoading
+                foreground: Nheko.colors.mid
+            }
+
+            Text {
+                id: errorText
+
+                color: "red"
+                visible: opacity > 0
+                opacity: 0
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            SequentialAnimation {
+                id: hideErrorAnimation
+
+                running: false
+
+                PauseAnimation {
+                    duration: 4000
+                }
+
+                NumberAnimation {
+                    target: errorText
+                    property: 'opacity'
+                    to: 0
+                    duration: 1000
+                }
+
+            }
+
+            Connections {
+                function onDisplayError(errorMessage) {
+                    errorText.text = errorMessage;
+                    errorText.opacity = 1;
+                    hideErrorAnimation.restart();
+                }
+
+                target: profile
+            }
+
+            TextInput {
+                id: displayUsername
+
+                property bool isUsernameEditingAllowed
+
+                readOnly: !isUsernameEditingAllowed
+                text: profile.displayName
+                font.pixelSize: 20
+                color: TimelineManager.userColor(profile.userid, Nheko.colors.window)
+                font.bold: true
+                Layout.alignment: Qt.AlignHCenter
+                selectByMouse: true
+                onAccepted: {
+                    profile.changeUsername(displayUsername.text);
+                    displayUsername.isUsernameEditingAllowed = false;
+                }
+
+                ImageButton {
+                    visible: profile.isSelf
+                    anchors.leftMargin: Nheko.paddingSmall
+                    anchors.left: displayUsername.right
+                    anchors.verticalCenter: displayUsername.verticalCenter
+                    hoverEnabled: true
+                    ToolTip.visible: hovered
+                    ToolTip.text: profile.isGlobalUserProfile ? qsTr("Change display name globally.") : qsTr("Change display name. Will only apply to this room.")
+                    image: displayUsername.isUsernameEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
+                    onClicked: {
+                        if (displayUsername.isUsernameEditingAllowed) {
+                            profile.changeUsername(displayUsername.text);
+                            displayUsername.isUsernameEditingAllowed = false;
+                        } else {
+                            displayUsername.isUsernameEditingAllowed = true;
+                            displayUsername.focus = true;
+                            displayUsername.selectAll();
+                        }
+                    }
+                }
+
+            }
+
+            MatrixText {
+                text: profile.userid
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            RowLayout {
+                visible: !profile.isGlobalUserProfile
+                Layout.alignment: Qt.AlignHCenter
+                spacing: Nheko.paddingSmall
+
+                MatrixText {
+                    id: displayRoomname
+
+                    text: qsTr("Room: %1").arg(profile.room ? profile.room.roomName : "")
+                    ToolTip.text: qsTr("This is a room-specific profile. The user's name and avatar may be different from their global versions.")
+                    ToolTip.visible: ma.hovered
+
+                    HoverHandler {
+                        id: ma
+                    }
+
+                }
+
+                ImageButton {
+                    image: ":/icons/icons/ui/world.png"
+                    hoverEnabled: true
+                    ToolTip.visible: hovered
+                    ToolTip.text: qsTr("Open the global profile for this user.")
+                    onClicked: profile.openGlobalProfile()
+                }
+
+            }
+
+            Button {
+                id: verifyUserButton
+
+                text: qsTr("Verify")
+                Layout.alignment: Qt.AlignHCenter
+                enabled: profile.userVerified != Crypto.Verified
+                visible: profile.userVerified != Crypto.Verified && !profile.isSelf && profile.userVerificationEnabled
+                onClicked: profile.verify()
+            }
+
+            Image {
+                Layout.preferredHeight: 16
+                Layout.preferredWidth: 16
+                source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : Nheko.colors.buttonText)
+                visible: profile.userVerified != Crypto.Unverified
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            RowLayout {
+                // ImageButton{
+                //     image:":/icons/icons/ui/volume-off-indicator.png"
+                //     Layout.margins: {
+                //         left: 5
+                //         right: 5
+                //     }
+                //     ToolTip.visible: hovered
+                //     ToolTip.text: qsTr("Ignore messages from this user.")
+                //     onClicked : {
+                //         profile.ignoreUser()
+                //     }
+                // }
+
+                Layout.alignment: Qt.AlignHCenter
+                Layout.bottomMargin: 10
+                spacing: Nheko.paddingSmall
+
+                ImageButton {
+                    image: ":/icons/icons/ui/black-bubble-speech.png"
+                    hoverEnabled: true
+                    ToolTip.visible: hovered
+                    ToolTip.text: qsTr("Start a private chat.")
+                    onClicked: profile.startChat()
+                }
+
+                ImageButton {
+                    image: ":/icons/icons/ui/round-remove-button.png"
+                    hoverEnabled: true
+                    ToolTip.visible: hovered
+                    ToolTip.text: qsTr("Kick the user.")
+                    onClicked: profile.kickUser()
+                    visible: !profile.isGlobalUserProfile && profile.room.permissions.canKick()
+                }
+
+                ImageButton {
+                    image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
+                    hoverEnabled: true
+                    ToolTip.visible: hovered
+                    ToolTip.text: qsTr("Ban the user.")
+                    onClicked: profile.banUser()
+                    visible: !profile.isGlobalUserProfile && profile.room.permissions.canBan()
+                }
+
+                ImageButton {
+                    image: ":/icons/icons/ui/refresh.png"
+                    hoverEnabled: true
+                    ToolTip.visible: hovered
+                    ToolTip.text: qsTr("Refresh device list.")
+                    onClicked: profile.refreshDevices()
+                }
+
+            }
+
+        }
+
+        delegate: RowLayout {
+            required property int verificationStatus
+            required property string deviceId
+            required property string deviceName
+            required property string lastIp
+            required property var lastTs
+
+            width: devicelist.width
+            spacing: 4
+
+            ColumnLayout {
+                spacing: 0
+
+                RowLayout {
+                    Text {
+                        Layout.fillWidth: true
+                        Layout.alignment: Qt.AlignLeft
+                        elide: Text.ElideRight
+                        font.bold: true
+                        color: Nheko.colors.text
+                        text: deviceId
+                    }
+
+                    Image {
+                        Layout.preferredHeight: 16
+                        Layout.preferredWidth: 16
+                        visible: profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE
+                        source: {
+                            switch (verificationStatus) {
+                            case VerificationStatus.VERIFIED:
+                                return "image://colorimage/:/icons/icons/ui/lock.png?green";
+                            case VerificationStatus.UNVERIFIED:
+                                return "image://colorimage/:/icons/icons/ui/unlock.png?yellow";
+                            case VerificationStatus.SELF:
+                                return "image://colorimage/:/icons/icons/ui/checkmark.png?green";
+                            default:
+                                return "image://colorimage/:/icons/icons/ui/unlock.png?red";
+                            }
+                        }
+                    }
+
+                    ImageButton {
+                        Layout.alignment: Qt.AlignTop
+                        image: ":/icons/icons/ui/power-button-off.png"
+                        hoverEnabled: true
+                        ToolTip.visible: hovered
+                        ToolTip.text: qsTr("Sign out this device.")
+                        onClicked: profile.signOutDevice(deviceId)
+                        visible: profile.isSelf
+                    }
+
+                }
+
+                RowLayout {
+                    id: deviceNameRow
+
+                    property bool isEditingAllowed
+
+                    TextInput {
+                        id: deviceNameField
+
+                        readOnly: !deviceNameRow.isEditingAllowed
+                        text: deviceName
+                        color: Nheko.colors.text
+                        Layout.alignment: Qt.AlignLeft
+                        Layout.fillWidth: true
+                        selectByMouse: true
+                        onAccepted: {
+                            profile.changeDeviceName(deviceId, deviceNameField.text);
+                            deviceNameRow.isEditingAllowed = false;
+                        }
+                    }
+
+                    ImageButton {
+                        visible: profile.isSelf
+                        hoverEnabled: true
+                        ToolTip.visible: hovered
+                        ToolTip.text: qsTr("Change device name.")
+                        image: deviceNameRow.isEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
+                        onClicked: {
+                            if (deviceNameRow.isEditingAllowed) {
+                                profile.changeDeviceName(deviceId, deviceNameField.text);
+                                deviceNameRow.isEditingAllowed = false;
+                            } else {
+                                deviceNameRow.isEditingAllowed = true;
+                                deviceNameField.focus = true;
+                                deviceNameField.selectAll();
+                            }
+                        }
+                    }
+
+                }
+
+                Text {
+                    visible: profile.isSelf
+                    Layout.fillWidth: true
+                    Layout.alignment: Qt.AlignLeft
+                    elide: Text.ElideRight
+                    color: Nheko.colors.text
+                    text: qsTr("Last seen %1 from %2").arg(new Date(lastTs).toLocaleString(Locale.ShortFormat)).arg(lastIp ? lastIp : "???")
+                }
+
+            }
+
+            Image {
+                Layout.preferredHeight: 16
+                Layout.preferredWidth: 16
+                visible: !profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE
+                source: {
+                    switch (verificationStatus) {
+                    case VerificationStatus.VERIFIED:
+                        return "image://colorimage/:/icons/icons/ui/lock.png?green";
+                    case VerificationStatus.UNVERIFIED:
+                        return "image://colorimage/:/icons/icons/ui/unlock.png?yellow";
+                    case VerificationStatus.SELF:
+                        return "image://colorimage/:/icons/icons/ui/checkmark.png?green";
+                    default:
+                        return "image://colorimage/:/icons/icons/ui/unlock.png?red";
+                    }
+                }
+            }
+
+            Button {
+                id: verifyButton
+
+                visible: verificationStatus == VerificationStatus.UNVERIFIED && (profile.isSelf || !profile.userVerificationEnabled)
+                text: (verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
+                onClicked: {
+                    if (verificationStatus == VerificationStatus.VERIFIED)
+                        profile.unverify(deviceId);
+                    else
+                        profile.verify(deviceId);
+                }
+            }
+
+        }
+
+        footer: DialogButtonBox {
+            z: 2
+            width: devicelist.width
+            alignment: Qt.AlignRight
+            standardButtons: DialogButtonBox.Ok
+            onAccepted: userProfileDialog.close()
+
+            background: Rectangle {
+                anchors.fill: parent
+                color: Nheko.colors.window
+            }
+
+        }
+
+    }
+
+}
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index 354e340cde12a7c67885fae0cbce344850bbc16d..e83f8a5e16aa5e19e3d3c964f96ebab2569c51ee 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -72,7 +72,8 @@ Menu {
                 onVisibleChanged: {
                     if (visible)
                         forceActiveFocus();
-
+                    else
+                        clear();
                 }
 
                 Timer {
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index d44c5edf9e638cf1c83b8c7e48d0faa376663260..be69835655cfc0d6bbb646248249dd3460305777 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -34,14 +34,15 @@ Rectangle {
             width: Nheko.avatarSize
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
-            displayName: CallManager.callParty
+            userid: CallManager.callParty
+            displayName: CallManager.callPartyDisplayName
             onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
         }
 
         Label {
             Layout.leftMargin: 8
             font.pointSize: fontMetrics.font.pointSize * 1.1
-            text: CallManager.callParty
+            text: CallManager.callPartyDisplayName
             color: "#000000"
         }
 
diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml
index 253fa25c6d626ecad0ea1f367a9dfdbadca9971b..1bd5eb265c2a27cc570ee02e3635a3497ff282e3 100644
--- a/resources/qml/voip/CallInvite.qml
+++ b/resources/qml/voip/CallInvite.qml
@@ -40,7 +40,7 @@ Popup {
         Label {
             Layout.alignment: Qt.AlignCenter
             Layout.topMargin: msgView.height / 25
-            text: CallManager.callParty
+            text: CallManager.callPartyDisplayName
             font.pointSize: fontMetrics.font.pointSize * 2
             color: Nheko.colors.windowText
         }
@@ -50,7 +50,8 @@ Popup {
             width: msgView.height / 5
             height: msgView.height / 5
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
-            displayName: CallManager.callParty
+            userid: CallManager.callParty
+            displayName: CallManager.callPartyDisplayName
         }
 
         ColumnLayout {
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index f6c1ecde8dc5034b2297f57fd37e6a576e58c0ff..10f8367a7361084268ef6a9fcb4c9818f6a9ebb0 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -41,14 +41,15 @@ Rectangle {
             width: Nheko.avatarSize
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
-            displayName: CallManager.callParty
+            userid: CallManager.callParty
+            displayName: CallManager.callPartyDisplayName
             onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
         }
 
         Label {
             Layout.leftMargin: 8
             font.pointSize: fontMetrics.font.pointSize * 1.1
-            text: CallManager.callParty
+            text: CallManager.callPartyDisplayName
             color: "#000000"
         }
 
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 97932cc948e9f1331e08be54ecdebe41c6a842d1..c733012caddb7e5026fe488a05440dbeac319aed 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -79,6 +79,7 @@ Popup {
                 height: Nheko.avatarSize
                 url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
                 displayName: room.roomName
+                roomid: room.roomid
                 onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
             }
 
diff --git a/resources/res.qrc b/resources/res.qrc
index e1761cc0e4db9ce39712921e8c7155aecdca6f20..ccb5a6375e731e8900e38e2af91544dc6337c35c 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -73,6 +73,7 @@
         <file>icons/ui/screen-share.png</file>
         <file>icons/ui/toggle-camera-view.png</file>
         <file>icons/ui/video-call.png</file>
+        <file>icons/ui/refresh.png</file>
         <file>icons/emoji-categories/people.png</file>
         <file>icons/emoji-categories/people@2x.png</file>
         <file>icons/emoji-categories/nature.png</file>
@@ -138,31 +139,47 @@
         <file>qml/TopBar.qml</file>
         <file>qml/QuickSwitcher.qml</file>
         <file>qml/ForwardCompleter.qml</file>
+        <file>qml/SelfVerificationCheck.qml</file>
         <file>qml/TypingIndicator.qml</file>
-        <file>qml/RoomSettings.qml</file>
-        <file>qml/emoji/EmojiPicker.qml</file>
-        <file>qml/emoji/StickerPicker.qml</file>
-        <file>qml/UserProfile.qml</file>
-        <file>qml/delegates/MessageDelegate.qml</file>
+        <file>qml/NotificationWarning.qml</file>
+        <file>qml/components/AdaptiveLayout.qml</file>
+        <file>qml/components/AdaptiveLayoutElement.qml</file>
+        <file>qml/components/AvatarListTile.qml</file>
+        <file>qml/components/FlatButton.qml</file>
+        <file>qml/components/MainWindowDialog.qml</file>
         <file>qml/delegates/Encrypted.qml</file>
         <file>qml/delegates/FileMessage.qml</file>
         <file>qml/delegates/ImageMessage.qml</file>
+        <file>qml/delegates/MessageDelegate.qml</file>
         <file>qml/delegates/NoticeMessage.qml</file>
         <file>qml/delegates/Pill.qml</file>
         <file>qml/delegates/Placeholder.qml</file>
         <file>qml/delegates/PlayableMediaMessage.qml</file>
         <file>qml/delegates/Reply.qml</file>
         <file>qml/delegates/TextMessage.qml</file>
-        <file>qml/device-verification/Waiting.qml</file>
         <file>qml/device-verification/DeviceVerification.qml</file>
         <file>qml/device-verification/DigitVerification.qml</file>
         <file>qml/device-verification/EmojiVerification.qml</file>
-        <file>qml/device-verification/NewVerificationRequest.qml</file>
         <file>qml/device-verification/Failed.qml</file>
+        <file>qml/device-verification/NewVerificationRequest.qml</file>
         <file>qml/device-verification/Success.qml</file>
-        <file>qml/dialogs/InputDialog.qml</file>
-        <file>qml/dialogs/ImagePackSettingsDialog.qml</file>
+        <file>qml/device-verification/Waiting.qml</file>
         <file>qml/dialogs/ImagePackEditorDialog.qml</file>
+        <file>qml/dialogs/ImagePackSettingsDialog.qml</file>
+        <file>qml/dialogs/PhoneNumberInputDialog.qml</file>
+        <file>qml/dialogs/InputDialog.qml</file>
+        <file>qml/dialogs/InviteDialog.qml</file>
+        <file>qml/dialogs/JoinRoomDialog.qml</file>
+        <file>qml/dialogs/LeaveRoomDialog.qml</file>
+        <file>qml/dialogs/LogoutDialog.qml</file>
+        <file>qml/dialogs/RawMessageDialog.qml</file>
+        <file>qml/dialogs/ReadReceipts.qml</file>
+        <file>qml/dialogs/RoomDirectory.qml</file>
+        <file>qml/dialogs/RoomMembers.qml</file>
+        <file>qml/dialogs/RoomSettings.qml</file>
+        <file>qml/dialogs/UserProfile.qml</file>
+        <file>qml/emoji/EmojiPicker.qml</file>
+        <file>qml/emoji/StickerPicker.qml</file>
         <file>qml/ui/Ripple.qml</file>
         <file>qml/ui/Spinner.qml</file>
         <file>qml/ui/animations/BlinkAnimation.qml</file>
@@ -174,14 +191,6 @@
         <file>qml/voip/PlaceCall.qml</file>
         <file>qml/voip/ScreenShare.qml</file>
         <file>qml/voip/VideoCall.qml</file>
-        <file>qml/components/AdaptiveLayout.qml</file>
-        <file>qml/components/AdaptiveLayoutElement.qml</file>
-        <file>qml/components/AvatarListTile.qml</file>
-        <file>qml/components/FlatButton.qml</file>
-        <file>qml/RoomMembers.qml</file>
-        <file>qml/InviteDialog.qml</file>
-        <file>qml/ReadReceipts.qml</file>
-        <file>qml/RawMessageDialog.qml</file>
     </qresource>
     <qresource prefix="/media">
         <file>media/ring.ogg</file>
diff --git a/scripts/emoji_codegen.py b/scripts/emoji_codegen.py
index df5810366beae2fbeadfb0a8c87204616453a77e..88f711f3b758af035e3f1922d92ba07de7b876a7 100755
--- a/scripts/emoji_codegen.py
+++ b/scripts/emoji_codegen.py
@@ -54,7 +54,7 @@ if __name__ == '__main__':
     }
 
     current_category = ''
-    for line in open(filename, 'r'):
+    for line in open(filename, 'r', encoding="utf8"):
         if line.startswith('# group:'):
             current_category = line.split(':', 1)[1].strip()
 
diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp
index b9962cef0a984feed82bf3d300880e2786d8b953..177bf903e12a0b34937b087bf19a1988856318be 100644
--- a/src/AvatarProvider.cpp
+++ b/src/AvatarProvider.cpp
@@ -22,45 +22,44 @@ namespace AvatarProvider {
 void
 resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
 {
-        const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
+    const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
 
-        QPixmap pixmap;
-        if (avatarUrl.isEmpty()) {
-                callback(pixmap);
-                return;
-        }
+    QPixmap pixmap;
+    if (avatarUrl.isEmpty()) {
+        callback(pixmap);
+        return;
+    }
 
-        if (avatar_cache.find(cacheKey, &pixmap)) {
-                callback(pixmap);
-                return;
-        }
+    if (avatar_cache.find(cacheKey, &pixmap)) {
+        callback(pixmap);
+        return;
+    }
 
-        MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
-                                   QSize(size, size),
-                                   [callback, cacheKey, recv = QPointer<QObject>(receiver)](
-                                     QString, QSize, QImage img, QString) {
-                                           if (!recv)
-                                                   return;
+    MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
+                               QSize(size, size),
+                               [callback, cacheKey, recv = QPointer<QObject>(receiver)](
+                                 QString, QSize, QImage img, QString) {
+                                   if (!recv)
+                                       return;
 
-                                           auto proxy = std::make_shared<AvatarProxy>();
-                                           QObject::connect(proxy.get(),
-                                                            &AvatarProxy::avatarDownloaded,
-                                                            recv,
-                                                            [callback, cacheKey](QPixmap pm) {
-                                                                    if (!pm.isNull())
-                                                                            avatar_cache.insert(
-                                                                              cacheKey, pm);
-                                                                    callback(pm);
-                                                            });
+                                   auto proxy = std::make_shared<AvatarProxy>();
+                                   QObject::connect(proxy.get(),
+                                                    &AvatarProxy::avatarDownloaded,
+                                                    recv,
+                                                    [callback, cacheKey](QPixmap pm) {
+                                                        if (!pm.isNull())
+                                                            avatar_cache.insert(cacheKey, pm);
+                                                        callback(pm);
+                                                    });
 
-                                           if (img.isNull()) {
-                                                   emit proxy->avatarDownloaded(QPixmap{});
-                                                   return;
-                                           }
+                                   if (img.isNull()) {
+                                       emit proxy->avatarDownloaded(QPixmap{});
+                                       return;
+                                   }
 
-                                           auto pm = QPixmap::fromImage(std::move(img));
-                                           emit proxy->avatarDownloaded(pm);
-                                   });
+                                   auto pm = QPixmap::fromImage(std::move(img));
+                                   emit proxy->avatarDownloaded(pm);
+                               });
 }
 
 void
@@ -70,8 +69,8 @@ resolve(const QString &room_id,
         QObject *receiver,
         AvatarCallback callback)
 {
-        auto avatarUrl = cache::avatarUrl(room_id, user_id);
+    auto avatarUrl = cache::avatarUrl(room_id, user_id);
 
-        resolve(std::move(avatarUrl), size, receiver, callback);
+    resolve(std::move(avatarUrl), size, receiver, callback);
 }
 }
diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h
index 173a2fba3438414602acdec60ccff297b1f8792b..efd0f330b4e5e92b002b9271d71d21b94a6b095e 100644
--- a/src/AvatarProvider.h
+++ b/src/AvatarProvider.h
@@ -12,10 +12,10 @@ using AvatarCallback = std::function<void(QPixmap)>;
 
 class AvatarProxy : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 signals:
-        void avatarDownloaded(QPixmap pm);
+    void avatarDownloaded(QPixmap pm);
 };
 
 namespace AvatarProvider {
diff --git a/src/BlurhashProvider.cpp b/src/BlurhashProvider.cpp
index aef618a29947ed4cc56c38d7d03dc99853338be2..e905474a2c3c644fcb721ae6b4baae006cfe80fd 100644
--- a/src/BlurhashProvider.cpp
+++ b/src/BlurhashProvider.cpp
@@ -13,33 +13,33 @@
 void
 BlurhashResponse::run()
 {
-        if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) {
-                m_error = QStringLiteral("Blurhash needs size request");
-                emit finished();
-                return;
-        }
-        if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) {
-                m_image = QImage(m_requestedSize, QImage::Format_RGB32);
-                m_image.fill(QColor(0, 0, 0));
-                emit finished();
-                return;
-        }
-
-        auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(),
-                                        m_requestedSize.width(),
-                                        m_requestedSize.height());
-        if (decoded.image.empty()) {
-                m_error = QStringLiteral("Failed decode!");
-                emit finished();
-                return;
-        }
-
-        QImage image(decoded.image.data(),
-                     (int)decoded.width,
-                     (int)decoded.height,
-                     (int)decoded.width * 3,
-                     QImage::Format_RGB888);
-
-        m_image = image.copy();
+    if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) {
+        m_error = QStringLiteral("Blurhash needs size request");
         emit finished();
+        return;
+    }
+    if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) {
+        m_image = QImage(m_requestedSize, QImage::Format_RGB32);
+        m_image.fill(QColor(0, 0, 0));
+        emit finished();
+        return;
+    }
+
+    auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(),
+                                    m_requestedSize.width(),
+                                    m_requestedSize.height());
+    if (decoded.image.empty()) {
+        m_error = QStringLiteral("Failed decode!");
+        emit finished();
+        return;
+    }
+
+    QImage image(decoded.image.data(),
+                 (int)decoded.width,
+                 (int)decoded.height,
+                 (int)decoded.width * 3,
+                 QImage::Format_RGB888);
+
+    m_image = image.copy();
+    emit finished();
 }
diff --git a/src/BlurhashProvider.h b/src/BlurhashProvider.h
index ee89302c8fb427db4a5429001be6f1a919508ded..1c8351f27082cbbe4654fd36ae52ceb8bf06c59c 100644
--- a/src/BlurhashProvider.h
+++ b/src/BlurhashProvider.h
@@ -15,41 +15,41 @@ class BlurhashResponse
   , public QRunnable
 {
 public:
-        BlurhashResponse(const QString &id, const QSize &requestedSize)
+    BlurhashResponse(const QString &id, const QSize &requestedSize)
 
-          : m_id(id)
-          , m_requestedSize(requestedSize)
-        {
-                setAutoDelete(false);
-        }
+      : m_id(id)
+      , m_requestedSize(requestedSize)
+    {
+        setAutoDelete(false);
+    }
 
-        QQuickTextureFactory *textureFactory() const override
-        {
-                return QQuickTextureFactory::textureFactoryForImage(m_image);
-        }
-        QString errorString() const override { return m_error; }
+    QQuickTextureFactory *textureFactory() const override
+    {
+        return QQuickTextureFactory::textureFactoryForImage(m_image);
+    }
+    QString errorString() const override { return m_error; }
 
-        void run() override;
+    void run() override;
 
-        QString m_id, m_error;
-        QSize m_requestedSize;
-        QImage m_image;
+    QString m_id, m_error;
+    QSize m_requestedSize;
+    QImage m_image;
 };
 
 class BlurhashProvider
   : public QObject
   , public QQuickAsyncImageProvider
 {
-        Q_OBJECT
+    Q_OBJECT
 public slots:
-        QQuickImageResponse *requestImageResponse(const QString &id,
-                                                  const QSize &requestedSize) override
-        {
-                BlurhashResponse *response = new BlurhashResponse(id, requestedSize);
-                pool.start(response);
-                return response;
-        }
+    QQuickImageResponse *requestImageResponse(const QString &id,
+                                              const QSize &requestedSize) override
+    {
+        BlurhashResponse *response = new BlurhashResponse(id, requestedSize);
+        pool.start(response);
+        return response;
+    }
 
 private:
-        QThreadPool pool;
+    QThreadPool pool;
 };
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 8b8b298546601377ca469a17da0b8a00d54b1e74..58eb2630a047ef98e6212cfbf9f6acd7bbc0cfd1 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -13,6 +13,7 @@
 #include <QFile>
 #include <QHash>
 #include <QMap>
+#include <QMessageBox>
 #include <QStandardPaths>
 
 #if __has_include(<keychain.h>)
@@ -29,19 +30,19 @@
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MatrixClient.h"
-#include "Olm.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "encryption/Olm.h"
 
 //! Should be changed when a breaking change occurs in the cache format.
 //! This will reset client's data.
-static const std::string CURRENT_CACHE_FORMAT_VERSION("2020.10.20");
-static const std::string SECRET("secret");
+static const std::string CURRENT_CACHE_FORMAT_VERSION("2021.08.31");
 
 //! Keys used for the DB
 static const std::string_view NEXT_BATCH_KEY("next_batch");
 static const std::string_view OLM_ACCOUNT_KEY("olm_account");
 static const std::string_view CACHE_FORMAT_VERSION_KEY("cache_format_version");
+static const std::string_view CURRENT_ONLINE_BACKUP_VERSION("current_online_backup_version");
 
 constexpr size_t MAX_RESTORED_MESSAGES = 30'000;
 
@@ -96,55 +97,55 @@ std::unique_ptr<Cache> instance_ = nullptr;
 
 struct RO_txn
 {
-        ~RO_txn() { txn.reset(); }
-        operator MDB_txn *() const noexcept { return txn.handle(); }
-        operator lmdb::txn &() noexcept { return txn; }
+    ~RO_txn() { txn.reset(); }
+    operator MDB_txn *() const noexcept { return txn.handle(); }
+    operator lmdb::txn &() noexcept { return txn; }
 
-        lmdb::txn &txn;
+    lmdb::txn &txn;
 };
 
 RO_txn
 ro_txn(lmdb::env &env)
 {
-        thread_local lmdb::txn txn     = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
-        thread_local int reuse_counter = 0;
+    thread_local lmdb::txn txn     = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+    thread_local int reuse_counter = 0;
 
-        if (reuse_counter >= 100 || txn.env() != env.handle()) {
-                txn.abort();
-                txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
-                reuse_counter = 0;
-        } else if (reuse_counter > 0) {
-                try {
-                        txn.renew();
-                } catch (...) {
-                        txn.abort();
-                        txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
-                        reuse_counter = 0;
-                }
+    if (reuse_counter >= 100 || txn.env() != env.handle()) {
+        txn.abort();
+        txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+        reuse_counter = 0;
+    } else if (reuse_counter > 0) {
+        try {
+            txn.renew();
+        } catch (...) {
+            txn.abort();
+            txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+            reuse_counter = 0;
         }
-        reuse_counter++;
+    }
+    reuse_counter++;
 
-        return RO_txn{txn};
+    return RO_txn{txn};
 }
 
 template<class T>
 bool
 containsStateUpdates(const T &e)
 {
-        return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
+    return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
 }
 
 bool
 containsStateUpdates(const mtx::events::collections::StrippedEvents &e)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        return std::holds_alternative<StrippedEvent<state::Avatar>>(e) ||
-               std::holds_alternative<StrippedEvent<CanonicalAlias>>(e) ||
-               std::holds_alternative<StrippedEvent<Name>>(e) ||
-               std::holds_alternative<StrippedEvent<Member>>(e) ||
-               std::holds_alternative<StrippedEvent<Topic>>(e);
+    return std::holds_alternative<StrippedEvent<state::Avatar>>(e) ||
+           std::holds_alternative<StrippedEvent<CanonicalAlias>>(e) ||
+           std::holds_alternative<StrippedEvent<Name>>(e) ||
+           std::holds_alternative<StrippedEvent<Member>>(e) ||
+           std::holds_alternative<StrippedEvent<Topic>>(e);
 }
 
 bool
@@ -152,45 +153,45 @@ Cache::isHiddenEvent(lmdb::txn &txn,
                      mtx::events::collections::TimelineEvents e,
                      const std::string &room_id)
 {
-        using namespace mtx::events;
+    using namespace mtx::events;
 
-        // Always hide edits
-        if (mtx::accessors::relations(e).replaces())
-                return true;
-
-        if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
-                MegolmSessionIndex index;
-                index.room_id    = room_id;
-                index.session_id = encryptedEvent->content.session_id;
-                index.sender_key = encryptedEvent->content.sender_key;
-
-                auto result = olm::decryptEvent(index, *encryptedEvent, true);
-                if (!result.error)
-                        e = result.event.value();
-        }
+    // Always hide edits
+    if (mtx::accessors::relations(e).replaces())
+        return true;
 
-        mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents;
-        hiddenEvents.hidden_event_types = {
-          EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
-
-        if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, ""))
-                hiddenEvents =
-                  std::move(std::get<mtx::events::AccountDataEvent<
-                              mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp)
-                              .content);
-        if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id))
-                hiddenEvents =
-                  std::move(std::get<mtx::events::AccountDataEvent<
-                              mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp)
-                              .content);
-
-        return std::visit(
-          [hiddenEvents](const auto &ev) {
-                  return std::any_of(hiddenEvents.hidden_event_types.begin(),
-                                     hiddenEvents.hidden_event_types.end(),
-                                     [ev](EventType type) { return type == ev.type; });
-          },
-          e);
+    if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
+        MegolmSessionIndex index;
+        index.room_id    = room_id;
+        index.session_id = encryptedEvent->content.session_id;
+        index.sender_key = encryptedEvent->content.sender_key;
+
+        auto result = olm::decryptEvent(index, *encryptedEvent, true);
+        if (!result.error)
+            e = result.event.value();
+    }
+
+    mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents;
+    hiddenEvents.hidden_event_types = {
+      EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
+
+    if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, ""))
+        hiddenEvents =
+          std::move(std::get<mtx::events::AccountDataEvent<
+                      mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp)
+                      .content);
+    if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id))
+        hiddenEvents =
+          std::move(std::get<mtx::events::AccountDataEvent<
+                      mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp)
+                      .content);
+
+    return std::visit(
+      [hiddenEvents](const auto &ev) {
+          return std::any_of(hiddenEvents.hidden_event_types.begin(),
+                             hiddenEvents.hidden_event_types.end(),
+                             [ev](EventType type) { return type == ev.type; });
+      },
+      e);
 }
 
 Cache::Cache(const QString &userId, QObject *parent)
@@ -198,217 +199,221 @@ Cache::Cache(const QString &userId, QObject *parent)
   , env_{nullptr}
   , localUserId_{userId}
 {
-        setup();
-        connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
+    setup();
+    connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
+    connect(
+      this,
+      &Cache::verificationStatusChanged,
+      this,
+      [this](const std::string &u) {
+          if (u == localUserId_.toStdString()) {
+              auto status = verificationStatus(u);
+              emit selfVerificationStatusChanged();
+          }
+      },
+      Qt::QueuedConnection);
 }
 
 void
 Cache::setup()
 {
-        auto settings = UserSettings::instance();
+    auto settings = UserSettings::instance();
 
-        nhlog::db()->debug("setting up cache");
+    nhlog::db()->debug("setting up cache");
 
-        // Previous location of the cache directory
-        auto oldCache = QString("%1/%2%3")
-                          .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
-                          .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
-                          .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
+    // Previous location of the cache directory
+    auto oldCache = QString("%1/%2%3")
+                      .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                      .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
+                      .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
 
-        cacheDirectory_ = QString("%1/%2%3")
-                            .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))
-                            .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
-                            .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
+    cacheDirectory_ = QString("%1/%2%3")
+                        .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))
+                        .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
+                        .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
 
-        bool isInitial = !QFile::exists(cacheDirectory_);
+    bool isInitial = !QFile::exists(cacheDirectory_);
 
-        // NOTE: If both cache directories exist it's better to do nothing: it
-        // could mean a previous migration failed or was interrupted.
-        bool needsMigration = isInitial && QFile::exists(oldCache);
+    // NOTE: If both cache directories exist it's better to do nothing: it
+    // could mean a previous migration failed or was interrupted.
+    bool needsMigration = isInitial && QFile::exists(oldCache);
 
-        if (needsMigration) {
-                nhlog::db()->info("found old state directory, migrating");
-                if (!QDir().rename(oldCache, cacheDirectory_)) {
-                        throw std::runtime_error(("Unable to migrate the old state directory (" +
-                                                  oldCache + ") to the new location (" +
-                                                  cacheDirectory_ + ")")
-                                                   .toStdString()
-                                                   .c_str());
-                }
-                nhlog::db()->info("completed state migration");
+    if (needsMigration) {
+        nhlog::db()->info("found old state directory, migrating");
+        if (!QDir().rename(oldCache, cacheDirectory_)) {
+            throw std::runtime_error(("Unable to migrate the old state directory (" + oldCache +
+                                      ") to the new location (" + cacheDirectory_ + ")")
+                                       .toStdString()
+                                       .c_str());
         }
+        nhlog::db()->info("completed state migration");
+    }
 
-        env_ = lmdb::env::create();
-        env_.set_mapsize(DB_SIZE);
-        env_.set_max_dbs(MAX_DBS);
+    env_ = lmdb::env::create();
+    env_.set_mapsize(DB_SIZE);
+    env_.set_max_dbs(MAX_DBS);
 
-        if (isInitial) {
-                nhlog::db()->info("initializing LMDB");
+    if (isInitial) {
+        nhlog::db()->info("initializing LMDB");
 
-                if (!QDir().mkpath(cacheDirectory_)) {
-                        throw std::runtime_error(
-                          ("Unable to create state directory:" + cacheDirectory_)
-                            .toStdString()
-                            .c_str());
-                }
+        if (!QDir().mkpath(cacheDirectory_)) {
+            throw std::runtime_error(
+              ("Unable to create state directory:" + cacheDirectory_).toStdString().c_str());
         }
+    }
 
-        try {
-                // NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
-                // it can really mess up our database, so we shouldn't. For now, hopefully
-                // NOMETASYNC is fast enough.
-                env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC);
-        } catch (const lmdb::error &e) {
-                if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
-                        throw std::runtime_error("LMDB initialization failed" +
-                                                 std::string(e.what()));
-                }
+    try {
+        // NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
+        // it can really mess up our database, so we shouldn't. For now, hopefully
+        // NOMETASYNC is fast enough.
+        env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC);
+    } catch (const lmdb::error &e) {
+        if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
+            throw std::runtime_error("LMDB initialization failed" + std::string(e.what()));
+        }
 
-                nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
+        nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
 
-                QDir stateDir(cacheDirectory_);
+        QDir stateDir(cacheDirectory_);
 
-                for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
-                        if (!stateDir.remove(file))
-                                throw std::runtime_error(
-                                  ("Unable to delete file " + file).toStdString().c_str());
-                }
-                env_.open(cacheDirectory_.toStdString().c_str());
+        for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
+            if (!stateDir.remove(file))
+                throw std::runtime_error(("Unable to delete file " + file).toStdString().c_str());
         }
+        env_.open(cacheDirectory_.toStdString().c_str());
+    }
 
-        auto txn          = lmdb::txn::begin(env_);
-        syncStateDb_      = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
-        roomsDb_          = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
-        spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT);
-        spacesParentsDb_  = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT);
-        invitesDb_        = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
-        readReceiptsDb_   = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
-        notificationsDb_  = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
-
-        // Device management
-        devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
-        deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
-
-        // Session management
-        inboundMegolmSessionDb_  = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
-        outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
-        megolmSessionDataDb_     = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
-
-        // What rooms are encrypted
-        encryptedRooms_                      = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
-        [[maybe_unused]] auto verificationDb = getVerificationDb(txn);
-        [[maybe_unused]] auto userKeysDb     = getUserKeysDb(txn);
+    auto txn          = lmdb::txn::begin(env_);
+    syncStateDb_      = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
+    roomsDb_          = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
+    spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT);
+    spacesParentsDb_  = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT);
+    invitesDb_        = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
+    readReceiptsDb_   = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
+    notificationsDb_  = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
 
-        txn.commit();
+    // Device management
+    devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
+    deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
+
+    // Session management
+    inboundMegolmSessionDb_  = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+    outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+    megolmSessionDataDb_     = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
 
-        databaseReady_ = true;
+    // What rooms are encrypted
+    encryptedRooms_                      = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+    [[maybe_unused]] auto verificationDb = getVerificationDb(txn);
+    [[maybe_unused]] auto userKeysDb     = getUserKeysDb(txn);
+
+    txn.commit();
+
+    databaseReady_ = true;
 }
 
 void
 Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
 {
-        nhlog::db()->info("mark room {} as encrypted", room_id);
+    nhlog::db()->info("mark room {} as encrypted", room_id);
 
-        encryptedRooms_.put(txn, room_id, "0");
+    encryptedRooms_.put(txn, room_id, "0");
 }
 
 bool
 Cache::isRoomEncrypted(const std::string &room_id)
 {
-        std::string_view unused;
+    std::string_view unused;
 
-        auto txn = ro_txn(env_);
-        auto res = encryptedRooms_.get(txn, room_id, unused);
+    auto txn = ro_txn(env_);
+    auto res = encryptedRooms_.get(txn, room_id, unused);
 
-        return res;
+    return res;
 }
 
 std::optional<mtx::events::state::Encryption>
 Cache::roomEncryptionSettings(const std::string &room_id)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        try {
-                auto txn      = ro_txn(env_);
-                auto statesdb = getStatesDb(txn, room_id);
-                std::string_view event;
-                bool res =
-                  statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event);
-
-                if (res) {
-                        try {
-                                StateEvent<Encryption> msg = json::parse(event);
-
-                                return msg.content;
-                        } catch (const json::exception &e) {
-                                nhlog::db()->warn("failed to parse m.room.encryption event: {}",
-                                                  e.what());
-                                return Encryption{};
-                        }
-                }
-        } catch (lmdb::error &) {
+    try {
+        auto txn      = ro_txn(env_);
+        auto statesdb = getStatesDb(txn, room_id);
+        std::string_view event;
+        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event);
+
+        if (res) {
+            try {
+                StateEvent<Encryption> msg = json::parse(event);
+
+                return msg.content;
+            } catch (const json::exception &e) {
+                nhlog::db()->warn("failed to parse m.room.encryption event: {}", e.what());
+                return Encryption{};
+            }
         }
+    } catch (lmdb::error &) {
+    }
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 mtx::crypto::ExportedSessionKeys
 Cache::exportSessionKeys()
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        ExportedSessionKeys keys;
+    ExportedSessionKeys keys;
 
-        auto txn    = ro_txn(env_);
-        auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
+    auto txn    = ro_txn(env_);
+    auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
 
-        std::string_view key, value;
-        while (cursor.get(key, value, MDB_NEXT)) {
-                ExportedSession exported;
-                MegolmSessionIndex index;
+    std::string_view key, value;
+    while (cursor.get(key, value, MDB_NEXT)) {
+        ExportedSession exported;
+        MegolmSessionIndex index;
 
-                auto saved_session = unpickle<InboundSessionObject>(std::string(value), SECRET);
+        auto saved_session = unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
 
-                try {
-                        index = nlohmann::json::parse(key).get<MegolmSessionIndex>();
-                } catch (const nlohmann::json::exception &e) {
-                        nhlog::db()->critical("failed to export megolm session: {}", e.what());
-                        continue;
-                }
+        try {
+            index = nlohmann::json::parse(key).get<MegolmSessionIndex>();
+        } catch (const nlohmann::json::exception &e) {
+            nhlog::db()->critical("failed to export megolm session: {}", e.what());
+            continue;
+        }
 
-                exported.room_id     = index.room_id;
-                exported.sender_key  = index.sender_key;
-                exported.session_id  = index.session_id;
-                exported.session_key = export_session(saved_session.get(), -1);
+        exported.room_id     = index.room_id;
+        exported.sender_key  = index.sender_key;
+        exported.session_id  = index.session_id;
+        exported.session_key = export_session(saved_session.get(), -1);
 
-                keys.sessions.push_back(exported);
-        }
+        keys.sessions.push_back(exported);
+    }
 
-        cursor.close();
+    cursor.close();
 
-        return keys;
+    return keys;
 }
 
 void
 Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
 {
-        for (const auto &s : keys.sessions) {
-                MegolmSessionIndex index;
-                index.room_id    = s.room_id;
-                index.session_id = s.session_id;
-                index.sender_key = s.sender_key;
+    for (const auto &s : keys.sessions) {
+        MegolmSessionIndex index;
+        index.room_id    = s.room_id;
+        index.session_id = s.session_id;
+        index.sender_key = s.sender_key;
 
-                GroupSessionData data{};
-                data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain;
-                if (s.sender_claimed_keys.count("ed25519"))
-                        data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519");
+        GroupSessionData data{};
+        data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain;
+        if (s.sender_claimed_keys.count("ed25519"))
+            data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519");
 
-                auto exported_session = mtx::crypto::import_session(s.session_key);
+        auto exported_session = mtx::crypto::import_session(s.session_key);
 
-                saveInboundMegolmSession(index, std::move(exported_session), data);
-                ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
-        }
+        saveInboundMegolmSession(index, std::move(exported_session), data);
+        ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
+    }
 }
 
 //
@@ -420,65 +425,64 @@ Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
                                 mtx::crypto::InboundGroupSessionPtr session,
                                 const GroupSessionData &data)
 {
-        using namespace mtx::crypto;
-        const auto key     = json(index).dump();
-        const auto pickled = pickle<InboundSessionObject>(session.get(), SECRET);
+    using namespace mtx::crypto;
+    const auto key     = json(index).dump();
+    const auto pickled = pickle<InboundSessionObject>(session.get(), pickle_secret_);
 
-        auto txn = lmdb::txn::begin(env_);
+    auto txn = lmdb::txn::begin(env_);
 
-        std::string_view value;
-        if (inboundMegolmSessionDb_.get(txn, key, value)) {
-                auto oldSession = unpickle<InboundSessionObject>(std::string(value), SECRET);
-                if (olm_inbound_group_session_first_known_index(session.get()) >
-                    olm_inbound_group_session_first_known_index(oldSession.get())) {
-                        nhlog::crypto()->warn(
-                          "Not storing inbound session with newer first known index");
-                        return;
-                }
+    std::string_view value;
+    if (inboundMegolmSessionDb_.get(txn, key, value)) {
+        auto oldSession = unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
+        if (olm_inbound_group_session_first_known_index(session.get()) >
+            olm_inbound_group_session_first_known_index(oldSession.get())) {
+            nhlog::crypto()->warn("Not storing inbound session with newer first known index");
+            return;
         }
+    }
 
-        inboundMegolmSessionDb_.put(txn, key, pickled);
-        megolmSessionDataDb_.put(txn, key, json(data).dump());
-        txn.commit();
+    inboundMegolmSessionDb_.put(txn, key, pickled);
+    megolmSessionDataDb_.put(txn, key, json(data).dump());
+    txn.commit();
 }
 
 mtx::crypto::InboundGroupSessionPtr
 Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        try {
-                auto txn        = ro_txn(env_);
-                std::string key = json(index).dump();
-                std::string_view value;
+    try {
+        auto txn        = ro_txn(env_);
+        std::string key = json(index).dump();
+        std::string_view value;
 
-                if (inboundMegolmSessionDb_.get(txn, key, value)) {
-                        auto session = unpickle<InboundSessionObject>(std::string(value), SECRET);
-                        return session;
-                }
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
+        if (inboundMegolmSessionDb_.get(txn, key, value)) {
+            auto session = unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
+            return session;
         }
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
+    }
 
-        return nullptr;
+    return nullptr;
 }
 
 bool
 Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        try {
-                auto txn        = ro_txn(env_);
-                std::string key = json(index).dump();
-                std::string_view value;
+    try {
+        auto txn        = ro_txn(env_);
+        std::string key = json(index).dump();
+        std::string_view value;
 
-                return inboundMegolmSessionDb_.get(txn, key, value);
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
-        }
+        return inboundMegolmSessionDb_.get(txn, key, value);
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
+    }
 
-        return false;
+    return false;
 }
 
 void
@@ -486,42 +490,42 @@ Cache::updateOutboundMegolmSession(const std::string &room_id,
                                    const GroupSessionData &data_,
                                    mtx::crypto::OutboundGroupSessionPtr &ptr)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        if (!outboundMegolmSessionExists(room_id))
-                return;
+    if (!outboundMegolmSessionExists(room_id))
+        return;
 
-        GroupSessionData data = data_;
-        data.message_index    = olm_outbound_group_session_message_index(ptr.get());
-        MegolmSessionIndex index;
-        index.room_id    = room_id;
-        index.sender_key = olm::client()->identity_keys().ed25519;
-        index.session_id = mtx::crypto::session_id(ptr.get());
+    GroupSessionData data = data_;
+    data.message_index    = olm_outbound_group_session_message_index(ptr.get());
+    MegolmSessionIndex index;
+    index.room_id    = room_id;
+    index.sender_key = olm::client()->identity_keys().ed25519;
+    index.session_id = mtx::crypto::session_id(ptr.get());
 
-        // Save the updated pickled data for the session.
-        json j;
-        j["session"] = pickle<OutboundSessionObject>(ptr.get(), SECRET);
+    // Save the updated pickled data for the session.
+    json j;
+    j["session"] = pickle<OutboundSessionObject>(ptr.get(), pickle_secret_);
 
-        auto txn = lmdb::txn::begin(env_);
-        outboundMegolmSessionDb_.put(txn, room_id, j.dump());
-        megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    outboundMegolmSessionDb_.put(txn, room_id, j.dump());
+    megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
+    txn.commit();
 }
 
 void
 Cache::dropOutboundMegolmSession(const std::string &room_id)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        if (!outboundMegolmSessionExists(room_id))
-                return;
+    if (!outboundMegolmSessionExists(room_id))
+        return;
 
-        {
-                auto txn = lmdb::txn::begin(env_);
-                outboundMegolmSessionDb_.del(txn, room_id);
-                // don't delete session data, so that we can still share the session.
-                txn.commit();
-        }
+    {
+        auto txn = lmdb::txn::begin(env_);
+        outboundMegolmSessionDb_.del(txn, room_id);
+        // don't delete session data, so that we can still share the session.
+        txn.commit();
+    }
 }
 
 void
@@ -529,86 +533,86 @@ Cache::saveOutboundMegolmSession(const std::string &room_id,
                                  const GroupSessionData &data_,
                                  mtx::crypto::OutboundGroupSessionPtr &session)
 {
-        using namespace mtx::crypto;
-        const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET);
+    using namespace mtx::crypto;
+    const auto pickled = pickle<OutboundSessionObject>(session.get(), pickle_secret_);
 
-        GroupSessionData data = data_;
-        data.message_index    = olm_outbound_group_session_message_index(session.get());
-        MegolmSessionIndex index;
-        index.room_id    = room_id;
-        index.sender_key = olm::client()->identity_keys().ed25519;
-        index.session_id = mtx::crypto::session_id(session.get());
+    GroupSessionData data = data_;
+    data.message_index    = olm_outbound_group_session_message_index(session.get());
+    MegolmSessionIndex index;
+    index.room_id    = room_id;
+    index.sender_key = olm::client()->identity_keys().ed25519;
+    index.session_id = mtx::crypto::session_id(session.get());
 
-        json j;
-        j["session"] = pickled;
+    json j;
+    j["session"] = pickled;
 
-        auto txn = lmdb::txn::begin(env_);
-        outboundMegolmSessionDb_.put(txn, room_id, j.dump());
-        megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    outboundMegolmSessionDb_.put(txn, room_id, j.dump());
+    megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
+    txn.commit();
 }
 
 bool
 Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
 {
-        try {
-                auto txn = ro_txn(env_);
-                std::string_view value;
-                return outboundMegolmSessionDb_.get(txn, room_id, value);
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
-                return false;
-        }
+    try {
+        auto txn = ro_txn(env_);
+        std::string_view value;
+        return outboundMegolmSessionDb_.get(txn, room_id, value);
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
+        return false;
+    }
 }
 
 OutboundGroupSessionDataRef
 Cache::getOutboundMegolmSession(const std::string &room_id)
 {
-        try {
-                using namespace mtx::crypto;
-
-                auto txn = ro_txn(env_);
-                std::string_view value;
-                outboundMegolmSessionDb_.get(txn, room_id, value);
-                auto obj = json::parse(value);
+    try {
+        using namespace mtx::crypto;
 
-                OutboundGroupSessionDataRef ref{};
-                ref.session = unpickle<OutboundSessionObject>(obj.at("session"), SECRET);
+        auto txn = ro_txn(env_);
+        std::string_view value;
+        outboundMegolmSessionDb_.get(txn, room_id, value);
+        auto obj = json::parse(value);
 
-                MegolmSessionIndex index;
-                index.room_id    = room_id;
-                index.sender_key = olm::client()->identity_keys().ed25519;
-                index.session_id = mtx::crypto::session_id(ref.session.get());
+        OutboundGroupSessionDataRef ref{};
+        ref.session = unpickle<OutboundSessionObject>(obj.at("session"), pickle_secret_);
 
-                if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
-                        ref.data = nlohmann::json::parse(value).get<GroupSessionData>();
-                }
+        MegolmSessionIndex index;
+        index.room_id    = room_id;
+        index.sender_key = olm::client()->identity_keys().ed25519;
+        index.session_id = mtx::crypto::session_id(ref.session.get());
 
-                return ref;
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
-                return {};
+        if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
+            ref.data = nlohmann::json::parse(value).get<GroupSessionData>();
         }
+
+        return ref;
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
+        return {};
+    }
 }
 
 std::optional<GroupSessionData>
 Cache::getMegolmSessionData(const MegolmSessionIndex &index)
 {
-        try {
-                using namespace mtx::crypto;
-
-                auto txn = ro_txn(env_);
+    try {
+        using namespace mtx::crypto;
 
-                std::string_view value;
-                if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
-                        return nlohmann::json::parse(value).get<GroupSessionData>();
-                }
+        auto txn = ro_txn(env_);
 
-                return std::nullopt;
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what());
-                return std::nullopt;
+        std::string_view value;
+        if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
+            return nlohmann::json::parse(value).get<GroupSessionData>();
         }
+
+        return std::nullopt;
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what());
+        return std::nullopt;
+    }
 }
 //
 // OLM sessions.
@@ -619,287 +623,353 @@ Cache::saveOlmSession(const std::string &curve25519,
                       mtx::crypto::OlmSessionPtr session,
                       uint64_t timestamp)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        const auto pickled    = pickle<SessionObject>(session.get(), SECRET);
-        const auto session_id = mtx::crypto::session_id(session.get());
+    const auto pickled    = pickle<SessionObject>(session.get(), pickle_secret_);
+    const auto session_id = mtx::crypto::session_id(session.get());
 
-        StoredOlmSession stored_session;
-        stored_session.pickled_session = pickled;
-        stored_session.last_message_ts = timestamp;
+    StoredOlmSession stored_session;
+    stored_session.pickled_session = pickled;
+    stored_session.last_message_ts = timestamp;
 
-        db.put(txn, session_id, json(stored_session).dump());
+    db.put(txn, session_id, json(stored_session).dump());
 
-        txn.commit();
+    txn.commit();
 }
 
 std::optional<mtx::crypto::OlmSessionPtr>
 Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        std::string_view pickled;
-        bool found = db.get(txn, session_id, pickled);
+    std::string_view pickled;
+    bool found = db.get(txn, session_id, pickled);
 
-        txn.commit();
+    txn.commit();
 
-        if (found) {
-                auto data = json::parse(pickled).get<StoredOlmSession>();
-                return unpickle<SessionObject>(data.pickled_session, SECRET);
-        }
+    if (found) {
+        auto data = json::parse(pickled).get<StoredOlmSession>();
+        return unpickle<SessionObject>(data.pickled_session, pickle_secret_);
+    }
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 std::optional<mtx::crypto::OlmSessionPtr>
 Cache::getLatestOlmSession(const std::string &curve25519)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        std::string_view session_id, pickled_session;
+    std::string_view session_id, pickled_session;
 
-        std::optional<StoredOlmSession> currentNewest;
+    std::optional<StoredOlmSession> currentNewest;
 
-        auto cursor = lmdb::cursor::open(txn, db);
-        while (cursor.get(session_id, pickled_session, MDB_NEXT)) {
-                auto data = json::parse(pickled_session).get<StoredOlmSession>();
-                if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts)
-                        currentNewest = data;
-        }
-        cursor.close();
+    auto cursor = lmdb::cursor::open(txn, db);
+    while (cursor.get(session_id, pickled_session, MDB_NEXT)) {
+        auto data = json::parse(pickled_session).get<StoredOlmSession>();
+        if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts)
+            currentNewest = data;
+    }
+    cursor.close();
 
-        txn.commit();
+    txn.commit();
 
-        return currentNewest
-                 ? std::optional(unpickle<SessionObject>(currentNewest->pickled_session, SECRET))
-                 : std::nullopt;
+    return currentNewest ? std::optional(unpickle<SessionObject>(currentNewest->pickled_session,
+                                                                 pickle_secret_))
+                         : std::nullopt;
 }
 
 std::vector<std::string>
 Cache::getOlmSessions(const std::string &curve25519)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        std::string_view session_id, unused;
-        std::vector<std::string> res;
+    std::string_view session_id, unused;
+    std::vector<std::string> res;
 
-        auto cursor = lmdb::cursor::open(txn, db);
-        while (cursor.get(session_id, unused, MDB_NEXT))
-                res.emplace_back(session_id);
-        cursor.close();
+    auto cursor = lmdb::cursor::open(txn, db);
+    while (cursor.get(session_id, unused, MDB_NEXT))
+        res.emplace_back(session_id);
+    cursor.close();
 
-        txn.commit();
+    txn.commit();
 
-        return res;
+    return res;
 }
 
 void
 Cache::saveOlmAccount(const std::string &data)
 {
-        auto txn = lmdb::txn::begin(env_);
-        syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data);
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data);
+    txn.commit();
 }
 
 std::string
 Cache::restoreOlmAccount()
 {
+    auto txn = ro_txn(env_);
+
+    std::string_view pickled;
+    syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled);
+
+    return std::string(pickled.data(), pickled.size());
+}
+
+void
+Cache::saveBackupVersion(const OnlineBackupVersion &data)
+{
+    auto txn = lmdb::txn::begin(env_);
+    syncStateDb_.put(txn, CURRENT_ONLINE_BACKUP_VERSION, nlohmann::json(data).dump());
+    txn.commit();
+}
+
+void
+Cache::deleteBackupVersion()
+{
+    auto txn = lmdb::txn::begin(env_);
+    syncStateDb_.del(txn, CURRENT_ONLINE_BACKUP_VERSION);
+    txn.commit();
+}
+
+std::optional<OnlineBackupVersion>
+Cache::backupVersion()
+{
+    try {
         auto txn = ro_txn(env_);
-        std::string_view pickled;
-        syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled);
+        std::string_view v;
+        syncStateDb_.get(txn, CURRENT_ONLINE_BACKUP_VERSION, v);
+
+        return nlohmann::json::parse(v).get<OnlineBackupVersion>();
+    } catch (...) {
+        return std::nullopt;
+    }
+}
 
-        return std::string(pickled.data(), pickled.size());
+static void
+fatalSecretError()
+{
+    QMessageBox::critical(
+      ChatPage::instance(),
+      QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"),
+      QCoreApplication::translate(
+        "SecretStorage",
+        "Nheko could not connect to the secure storage to save encryption secrets to. This can "
+        "have multiple reasons. Check if your D-Bus service is running and you have configured a "
+        "service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If "
+        "you are having trouble, feel free to open an issue here: "
+        "https://github.com/Nheko-Reborn/nheko/issues"));
+
+    QCoreApplication::exit(1);
+    exit(1);
 }
 
 void
-Cache::storeSecret(const std::string name, const std::string secret)
-{
-        auto settings = UserSettings::instance();
-        auto job      = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
-        job->setAutoDelete(true);
-        job->setInsecureFallback(true);
-        job->setSettings(UserSettings::instance()->qsettings());
-
-        job->setKey(
-          "matrix." +
-          QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
-                    .toBase64()) +
-          "." + QString::fromStdString(name));
-
-        job->setTextData(QString::fromStdString(secret));
-
-        QObject::connect(
-          job,
-          &QKeychain::WritePasswordJob::finished,
-          this,
-          [name, this](QKeychain::Job *job) {
-                  if (job->error()) {
-                          nhlog::db()->warn("Storing secret '{}' failed: {}",
-                                            name,
-                                            job->errorString().toStdString());
-                  } else {
-                          // if we emit the signal directly, qtkeychain breaks and won't execute new
-                          // jobs. You can't start a job from the finish signal of a job.
-                          QTimer::singleShot(100, [this, name] { emit secretChanged(name); });
-                          nhlog::db()->info("Storing secret '{}' successful", name);
-                  }
-          },
-          Qt::ConnectionType::DirectConnection);
-        job->start();
+Cache::storeSecret(const std::string name, const std::string secret, bool internal)
+{
+    auto settings = UserSettings::instance();
+    auto job      = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
+    job->setAutoDelete(true);
+    job->setInsecureFallback(true);
+    job->setSettings(UserSettings::instance()->qsettings());
+
+    job->setKey(
+      (internal ? "nheko." : "matrix.") +
+      QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
+                .toBase64()) +
+      "." + QString::fromStdString(name));
+
+    job->setTextData(QString::fromStdString(secret));
+
+    QObject::connect(
+      job,
+      &QKeychain::WritePasswordJob::finished,
+      this,
+      [name, this](QKeychain::Job *job) {
+          if (job->error()) {
+              nhlog::db()->warn(
+                "Storing secret '{}' failed: {}", name, job->errorString().toStdString());
+              fatalSecretError();
+          } else {
+              // if we emit the signal directly, qtkeychain breaks and won't execute new
+              // jobs. You can't start a job from the finish signal of a job.
+              QTimer::singleShot(100, [this, name] { emit secretChanged(name); });
+              nhlog::db()->info("Storing secret '{}' successful", name);
+          }
+      },
+      Qt::ConnectionType::DirectConnection);
+    job->start();
 }
 
 void
-Cache::deleteSecret(const std::string name)
+Cache::deleteSecret(const std::string name, bool internal)
 {
-        auto settings = UserSettings::instance();
-        QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
-        job.setAutoDelete(false);
-        job.setInsecureFallback(true);
-        job.setSettings(UserSettings::instance()->qsettings());
+    auto settings = UserSettings::instance();
+    QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
+    job.setAutoDelete(false);
+    job.setInsecureFallback(true);
+    job.setSettings(UserSettings::instance()->qsettings());
 
-        job.setKey(
-          "matrix." +
-          QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
-                    .toBase64()) +
-          "." + QString::fromStdString(name));
+    job.setKey(
+      (internal ? "nheko." : "matrix.") +
+      QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
+                .toBase64()) +
+      "." + QString::fromStdString(name));
 
-        // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
-        // time!
-        QEventLoop loop;
-        job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
-        job.start();
-        loop.exec();
+    // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
+    // time!
+    QEventLoop loop;
+    job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+    job.start();
+    loop.exec();
 
-        emit secretChanged(name);
+    emit secretChanged(name);
 }
 
 std::optional<std::string>
-Cache::secret(const std::string name)
-{
-        auto settings = UserSettings::instance();
-        QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
-        job.setAutoDelete(false);
-        job.setInsecureFallback(true);
-        job.setSettings(UserSettings::instance()->qsettings());
-
-        job.setKey(
-          "matrix." +
-          QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
-                    .toBase64()) +
-          "." + QString::fromStdString(name));
-
-        // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
-        // time!
-        QEventLoop loop;
-        job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
-        job.start();
-        loop.exec();
-
-        const QString secret = job.textData();
-        if (job.error()) {
-                nhlog::db()->debug(
-                  "Restoring secret '{}' failed: {}", name, job.errorString().toStdString());
-                return std::nullopt;
-        }
-        if (secret.isEmpty()) {
-                nhlog::db()->debug("Restored empty secret '{}'.", name);
-                return std::nullopt;
+Cache::secret(const std::string name, bool internal)
+{
+    auto settings = UserSettings::instance();
+    QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
+    job.setAutoDelete(false);
+    job.setInsecureFallback(true);
+    job.setSettings(UserSettings::instance()->qsettings());
+
+    job.setKey(
+      (internal ? "nheko." : "matrix.") +
+      QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
+                .toBase64()) +
+      "." + QString::fromStdString(name));
+
+    // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
+    // time!
+    QEventLoop loop;
+    job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+    job.start();
+    loop.exec();
+
+    const QString secret = job.textData();
+    if (job.error()) {
+        if (job.error() == QKeychain::Error::EntryNotFound)
+            return std::nullopt;
+        nhlog::db()->error("Restoring secret '{}' failed ({}): {}",
+                           name,
+                           job.error(),
+                           job.errorString().toStdString());
+
+        fatalSecretError();
+        return std::nullopt;
+    }
+    if (secret.isEmpty()) {
+        nhlog::db()->debug("Restored empty secret '{}'.", name);
+        return std::nullopt;
+    }
+
+    return secret.toStdString();
+}
+
+std::string
+Cache::pickleSecret()
+{
+    if (pickle_secret_.empty()) {
+        auto s = secret("pickle_secret", true);
+        if (!s) {
+            this->pickle_secret_ = mtx::client::utils::random_token(64, true);
+            storeSecret("pickle_secret", pickle_secret_, true);
+        } else {
+            this->pickle_secret_ = *s;
         }
+    }
 
-        return secret.toStdString();
+    return pickle_secret_;
 }
 
 void
 Cache::removeInvite(lmdb::txn &txn, const std::string &room_id)
 {
-        invitesDb_.del(txn, room_id);
-        getInviteStatesDb(txn, room_id).drop(txn, true);
-        getInviteMembersDb(txn, room_id).drop(txn, true);
+    invitesDb_.del(txn, room_id);
+    getInviteStatesDb(txn, room_id).drop(txn, true);
+    getInviteMembersDb(txn, room_id).drop(txn, true);
 }
 
 void
 Cache::removeInvite(const std::string &room_id)
 {
-        auto txn = lmdb::txn::begin(env_);
-        removeInvite(txn, room_id);
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    removeInvite(txn, room_id);
+    txn.commit();
 }
 
 void
 Cache::removeRoom(lmdb::txn &txn, const std::string &roomid)
 {
-        roomsDb_.del(txn, roomid);
-        getStatesDb(txn, roomid).drop(txn, true);
-        getAccountDataDb(txn, roomid).drop(txn, true);
-        getMembersDb(txn, roomid).drop(txn, true);
+    roomsDb_.del(txn, roomid);
+    getStatesDb(txn, roomid).drop(txn, true);
+    getAccountDataDb(txn, roomid).drop(txn, true);
+    getMembersDb(txn, roomid).drop(txn, true);
 }
 
 void
 Cache::removeRoom(const std::string &roomid)
 {
-        auto txn = lmdb::txn::begin(env_, nullptr, 0);
-        roomsDb_.del(txn, roomid);
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_, nullptr, 0);
+    roomsDb_.del(txn, roomid);
+    txn.commit();
 }
 
 void
 Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token)
 {
-        syncStateDb_.put(txn, NEXT_BATCH_KEY, token);
-}
-
-void
-Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
-{
-        setNextBatchToken(txn, token.toStdString());
+    syncStateDb_.put(txn, NEXT_BATCH_KEY, token);
 }
 
 bool
 Cache::isInitialized()
 {
-        if (!env_.handle())
-                return false;
+    if (!env_.handle())
+        return false;
 
-        auto txn = ro_txn(env_);
-        std::string_view token;
+    auto txn = ro_txn(env_);
+    std::string_view token;
 
-        bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
+    bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
 
-        return res;
+    return res;
 }
 
 std::string
 Cache::nextBatchToken()
 {
-        if (!env_.handle())
-                throw lmdb::error("Env already closed", MDB_INVALID);
+    if (!env_.handle())
+        throw lmdb::error("Env already closed", MDB_INVALID);
 
-        auto txn = ro_txn(env_);
-        std::string_view token;
+    auto txn = ro_txn(env_);
+    std::string_view token;
 
-        bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
+    bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
 
-        if (result)
-                return std::string(token.data(), token.size());
-        else
-                return "";
+    if (result)
+        return std::string(token.data(), token.size());
+    else
+        return "";
 }
 
 void
 Cache::deleteData()
 {
+    if (this->databaseReady_) {
         this->databaseReady_ = false;
         // TODO: We need to remove the env_ while not accepting new requests.
         lmdb::dbi_close(env_, syncStateDb_);
@@ -920,596 +990,607 @@ Cache::deleteData()
         verification_storage.status.clear();
 
         if (!cacheDirectory_.isEmpty()) {
-                QDir(cacheDirectory_).removeRecursively();
-                nhlog::db()->info("deleted cache files from disk");
+            QDir(cacheDirectory_).removeRecursively();
+            nhlog::db()->info("deleted cache files from disk");
         }
 
         deleteSecret(mtx::secret_storage::secrets::megolm_backup_v1);
         deleteSecret(mtx::secret_storage::secrets::cross_signing_master);
         deleteSecret(mtx::secret_storage::secrets::cross_signing_user_signing);
         deleteSecret(mtx::secret_storage::secrets::cross_signing_self_signing);
+        deleteSecret("pickle_secret", true);
+    }
 }
 
 //! migrates db to the current format
 bool
 Cache::runMigrations()
 {
-        std::string stored_version;
-        {
-                auto txn = ro_txn(env_);
-
-                std::string_view current_version;
-                bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version);
-
-                if (!res)
-                        return false;
-
-                stored_version = std::string(current_version);
-        }
+    std::string stored_version;
+    {
+        auto txn = ro_txn(env_);
 
-        std::vector<std::pair<std::string, std::function<bool()>>> migrations{
-          {"2020.05.01",
-           [this]() {
-                   try {
-                           auto txn = lmdb::txn::begin(env_, nullptr);
-                           auto pending_receipts =
-                             lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
-                           lmdb::dbi_drop(txn, pending_receipts, true);
-                           txn.commit();
-                   } catch (const lmdb::error &) {
-                           nhlog::db()->critical(
-                             "Failed to delete pending_receipts database in migration!");
-                           return false;
-                   }
+        std::string_view current_version;
+        bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version);
 
-                   nhlog::db()->info("Successfully deleted pending receipts database.");
-                   return true;
-           }},
-          {"2020.07.05",
-           [this]() {
+        if (!res)
+            return false;
+
+        stored_version = std::string(current_version);
+    }
+
+    std::vector<std::pair<std::string, std::function<bool()>>> migrations{
+      {"2020.05.01",
+       [this]() {
+           try {
+               auto txn              = lmdb::txn::begin(env_, nullptr);
+               auto pending_receipts = lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
+               lmdb::dbi_drop(txn, pending_receipts, true);
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to delete pending_receipts database in migration!");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully deleted pending receipts database.");
+           return true;
+       }},
+      {"2020.07.05",
+       [this]() {
+           try {
+               auto txn      = lmdb::txn::begin(env_, nullptr);
+               auto room_ids = getRoomIds(txn);
+
+               for (const auto &room_id : room_ids) {
                    try {
-                           auto txn      = lmdb::txn::begin(env_, nullptr);
-                           auto room_ids = getRoomIds(txn);
-
-                           for (const auto &room_id : room_ids) {
-                                   try {
-                                           auto messagesDb = lmdb::dbi::open(
-                                             txn, std::string(room_id + "/messages").c_str());
-
-                                           // keep some old messages and batch token
-                                           {
-                                                   auto roomsCursor =
-                                                     lmdb::cursor::open(txn, messagesDb);
-                                                   std::string_view ts, stored_message;
-                                                   bool start = true;
-                                                   mtx::responses::Timeline oldMessages;
-                                                   while (roomsCursor.get(ts,
-                                                                          stored_message,
-                                                                          start ? MDB_FIRST
-                                                                                : MDB_NEXT)) {
-                                                           start = false;
-
-                                                           auto j = json::parse(std::string_view(
-                                                             stored_message.data(),
-                                                             stored_message.size()));
-
-                                                           if (oldMessages.prev_batch.empty())
-                                                                   oldMessages.prev_batch =
-                                                                     j["token"].get<std::string>();
-                                                           else if (j["token"] !=
-                                                                    oldMessages.prev_batch)
-                                                                   break;
-
-                                                           mtx::events::collections::TimelineEvent
-                                                             te;
-                                                           mtx::events::collections::from_json(
-                                                             j["event"], te);
-                                                           oldMessages.events.push_back(te.data);
-                                                   }
-                                                   // messages were stored in reverse order, so we
-                                                   // need to reverse them
-                                                   std::reverse(oldMessages.events.begin(),
-                                                                oldMessages.events.end());
-                                                   // save messages using the new method
-                                                   auto eventsDb = getEventsDb(txn, room_id);
-                                                   saveTimelineMessages(
-                                                     txn, eventsDb, room_id, oldMessages);
-                                           }
-
-                                           // delete old messages db
-                                           lmdb::dbi_drop(txn, messagesDb, true);
-                                   } catch (std::exception &e) {
-                                           nhlog::db()->error(
-                                             "While migrating messages from {}, ignoring error {}",
-                                             room_id,
-                                             e.what());
-                                   }
+                       auto messagesDb =
+                         lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str());
+
+                       // keep some old messages and batch token
+                       {
+                           auto roomsCursor = lmdb::cursor::open(txn, messagesDb);
+                           std::string_view ts, stored_message;
+                           bool start = true;
+                           mtx::responses::Timeline oldMessages;
+                           while (
+                             roomsCursor.get(ts, stored_message, start ? MDB_FIRST : MDB_NEXT)) {
+                               start = false;
+
+                               auto j = json::parse(
+                                 std::string_view(stored_message.data(), stored_message.size()));
+
+                               if (oldMessages.prev_batch.empty())
+                                   oldMessages.prev_batch = j["token"].get<std::string>();
+                               else if (j["token"] != oldMessages.prev_batch)
+                                   break;
+
+                               mtx::events::collections::TimelineEvent te;
+                               mtx::events::collections::from_json(j["event"], te);
+                               oldMessages.events.push_back(te.data);
                            }
-                           txn.commit();
-                   } catch (const lmdb::error &) {
-                           nhlog::db()->critical(
-                             "Failed to delete messages database in migration!");
-                           return false;
+                           // messages were stored in reverse order, so we
+                           // need to reverse them
+                           std::reverse(oldMessages.events.begin(), oldMessages.events.end());
+                           // save messages using the new method
+                           auto eventsDb = getEventsDb(txn, room_id);
+                           saveTimelineMessages(txn, eventsDb, room_id, oldMessages);
+                       }
+
+                       // delete old messages db
+                       lmdb::dbi_drop(txn, messagesDb, true);
+                   } catch (std::exception &e) {
+                       nhlog::db()->error(
+                         "While migrating messages from {}, ignoring error {}", room_id, e.what());
                    }
+               }
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to delete messages database in migration!");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully deleted pending receipts database.");
+           return true;
+       }},
+      {"2020.10.20",
+       [this]() {
+           try {
+               using namespace mtx::crypto;
+
+               auto txn = lmdb::txn::begin(env_);
+
+               auto mainDb = lmdb::dbi::open(txn, nullptr);
+
+               std::string_view dbName, ignored;
+               auto olmDbCursor = lmdb::cursor::open(txn, mainDb);
+               while (olmDbCursor.get(dbName, ignored, MDB_NEXT)) {
+                   // skip every db but olm session dbs
+                   nhlog::db()->debug("Db {}", dbName);
+                   if (dbName.find("olm_sessions/") != 0)
+                       continue;
+
+                   nhlog::db()->debug("Migrating {}", dbName);
+
+                   auto olmDb = lmdb::dbi::open(txn, std::string(dbName).c_str());
+
+                   std::string_view session_id, session_value;
+
+                   std::vector<std::pair<std::string, StoredOlmSession>> sessions;
+
+                   auto cursor = lmdb::cursor::open(txn, olmDb);
+                   while (cursor.get(session_id, session_value, MDB_NEXT)) {
+                       nhlog::db()->debug(
+                         "session_id {}, session_value {}", session_id, session_value);
+                       StoredOlmSession session;
+                       bool invalid = false;
+                       for (auto c : session_value)
+                           if (!isprint(c)) {
+                               invalid = true;
+                               break;
+                           }
+                       if (invalid)
+                           continue;
 
-                   nhlog::db()->info("Successfully deleted pending receipts database.");
-                   return true;
-           }},
-          {"2020.10.20",
-           [this]() {
-                   try {
-                           using namespace mtx::crypto;
-
-                           auto txn = lmdb::txn::begin(env_);
-
-                           auto mainDb = lmdb::dbi::open(txn, nullptr);
-
-                           std::string_view dbName, ignored;
-                           auto olmDbCursor = lmdb::cursor::open(txn, mainDb);
-                           while (olmDbCursor.get(dbName, ignored, MDB_NEXT)) {
-                                   // skip every db but olm session dbs
-                                   nhlog::db()->debug("Db {}", dbName);
-                                   if (dbName.find("olm_sessions/") != 0)
-                                           continue;
-
-                                   nhlog::db()->debug("Migrating {}", dbName);
-
-                                   auto olmDb = lmdb::dbi::open(txn, std::string(dbName).c_str());
-
-                                   std::string_view session_id, session_value;
-
-                                   std::vector<std::pair<std::string, StoredOlmSession>> sessions;
-
-                                   auto cursor = lmdb::cursor::open(txn, olmDb);
-                                   while (cursor.get(session_id, session_value, MDB_NEXT)) {
-                                           nhlog::db()->debug("session_id {}, session_value {}",
-                                                              session_id,
-                                                              session_value);
-                                           StoredOlmSession session;
-                                           bool invalid = false;
-                                           for (auto c : session_value)
-                                                   if (!isprint(c)) {
-                                                           invalid = true;
-                                                           break;
-                                                   }
-                                           if (invalid)
-                                                   continue;
-
-                                           nhlog::db()->debug("Not skipped");
-
-                                           session.pickled_session = session_value;
-                                           sessions.emplace_back(session_id, session);
-                                   }
-                                   cursor.close();
+                       nhlog::db()->debug("Not skipped");
 
-                                   olmDb.drop(txn, true);
+                       session.pickled_session = session_value;
+                       sessions.emplace_back(session_id, session);
+                   }
+                   cursor.close();
 
-                                   auto newDbName = std::string(dbName);
-                                   newDbName.erase(0, sizeof("olm_sessions") - 1);
-                                   newDbName = "olm_sessions.v2" + newDbName;
+                   olmDb.drop(txn, true);
 
-                                   auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE);
+                   auto newDbName = std::string(dbName);
+                   newDbName.erase(0, sizeof("olm_sessions") - 1);
+                   newDbName = "olm_sessions.v2" + newDbName;
 
-                                   for (const auto &[key, value] : sessions) {
-                                           // nhlog::db()->debug("{}\n{}", key, json(value).dump());
-                                           newDb.put(txn, key, json(value).dump());
-                                   }
-                           }
-                           olmDbCursor.close();
+                   auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE);
 
-                           txn.commit();
-                   } catch (const lmdb::error &) {
-                           nhlog::db()->critical("Failed to migrate olm sessions,");
-                           return false;
+                   for (const auto &[key, value] : sessions) {
+                       // nhlog::db()->debug("{}\n{}", key, json(value).dump());
+                       newDb.put(txn, key, json(value).dump());
                    }
+               }
+               olmDbCursor.close();
+
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to migrate olm sessions,");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully migrated olm sessions.");
+           return true;
+       }},
+      {"2021.08.22",
+       [this]() {
+           try {
+               auto txn      = lmdb::txn::begin(env_, nullptr);
+               auto try_drop = [&txn](const std::string &dbName) {
+                   try {
+                       auto db = lmdb::dbi::open(txn, dbName.c_str());
+                       db.drop(txn, true);
+                   } catch (std::exception &e) {
+                       nhlog::db()->warn("Failed to drop '{}': {}", dbName, e.what());
+                   }
+               };
+
+               auto room_ids = getRoomIds(txn);
+
+               for (const auto &room : room_ids) {
+                   try_drop(room + "/state");
+                   try_drop(room + "/state_by_key");
+                   try_drop(room + "/account_data");
+                   try_drop(room + "/members");
+                   try_drop(room + "/mentions");
+                   try_drop(room + "/events");
+                   try_drop(room + "/event_order");
+                   try_drop(room + "/event2order");
+                   try_drop(room + "/msg2order");
+                   try_drop(room + "/order2msg");
+                   try_drop(room + "/pending");
+                   try_drop(room + "/related");
+               }
+
+               // clear db, don't delete
+               roomsDb_.drop(txn, false);
+               setNextBatchToken(txn, "");
+
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to clear cache!");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully cleared the cache. Will do a clean sync after startup.");
+           return true;
+       }},
+      {"2021.08.31",
+       [this]() {
+           storeSecret("pickle_secret", "secret", true);
+           return true;
+       }},
+    };
+
+    nhlog::db()->info("Running migrations, this may take a while!");
+    for (const auto &[target_version, migration] : migrations) {
+        if (target_version > stored_version)
+            if (!migration()) {
+                nhlog::db()->critical("migration failure!");
+                return false;
+            }
+    }
+    nhlog::db()->info("Migrations finished.");
 
-                   nhlog::db()->info("Successfully migrated olm sessions.");
-                   return true;
-           }},
-        };
-
-        nhlog::db()->info("Running migrations, this may take a while!");
-        for (const auto &[target_version, migration] : migrations) {
-                if (target_version > stored_version)
-                        if (!migration()) {
-                                nhlog::db()->critical("migration failure!");
-                                return false;
-                        }
-        }
-        nhlog::db()->info("Migrations finished.");
-
-        setCurrentFormat();
-        return true;
+    setCurrentFormat();
+    return true;
 }
 
 cache::CacheVersion
 Cache::formatVersion()
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::string_view current_version;
-        bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version);
+    std::string_view current_version;
+    bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version);
 
-        if (!res)
-                return cache::CacheVersion::Older;
+    if (!res)
+        return cache::CacheVersion::Older;
 
-        std::string stored_version(current_version.data(), current_version.size());
+    std::string stored_version(current_version.data(), current_version.size());
 
-        if (stored_version < CURRENT_CACHE_FORMAT_VERSION)
-                return cache::CacheVersion::Older;
-        else if (stored_version > CURRENT_CACHE_FORMAT_VERSION)
-                return cache::CacheVersion::Older;
-        else
-                return cache::CacheVersion::Current;
+    if (stored_version < CURRENT_CACHE_FORMAT_VERSION)
+        return cache::CacheVersion::Older;
+    else if (stored_version > CURRENT_CACHE_FORMAT_VERSION)
+        return cache::CacheVersion::Older;
+    else
+        return cache::CacheVersion::Current;
 }
 
 void
 Cache::setCurrentFormat()
 {
-        auto txn = lmdb::txn::begin(env_);
+    auto txn = lmdb::txn::begin(env_);
 
-        syncStateDb_.put(txn, CACHE_FORMAT_VERSION_KEY, CURRENT_CACHE_FORMAT_VERSION);
+    syncStateDb_.put(txn, CACHE_FORMAT_VERSION_KEY, CURRENT_CACHE_FORMAT_VERSION);
 
-        txn.commit();
+    txn.commit();
 }
 
 CachedReceipts
 Cache::readReceipts(const QString &event_id, const QString &room_id)
 {
-        CachedReceipts receipts;
-
-        ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
-        nlohmann::json json_key = receipt_key;
+    CachedReceipts receipts;
 
-        try {
-                auto txn = ro_txn(env_);
-                auto key = json_key.dump();
+    ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
+    nlohmann::json json_key = receipt_key;
 
-                std::string_view value;
+    try {
+        auto txn = ro_txn(env_);
+        auto key = json_key.dump();
 
-                bool res = readReceiptsDb_.get(txn, key, value);
+        std::string_view value;
 
-                if (res) {
-                        auto json_response =
-                          json::parse(std::string_view(value.data(), value.size()));
-                        auto values = json_response.get<std::map<std::string, uint64_t>>();
+        bool res = readReceiptsDb_.get(txn, key, value);
 
-                        for (const auto &v : values)
-                                // timestamp, user_id
-                                receipts.emplace(v.second, v.first);
-                }
+        if (res) {
+            auto json_response = json::parse(std::string_view(value.data(), value.size()));
+            auto values        = json_response.get<std::map<std::string, uint64_t>>();
 
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("readReceipts: {}", e.what());
+            for (const auto &v : values)
+                // timestamp, user_id
+                receipts.emplace(v.second, v.first);
         }
 
-        return receipts;
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("readReceipts: {}", e.what());
+    }
+
+    return receipts;
 }
 
 void
 Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
 {
-        auto user_id = this->localUserId_.toStdString();
-        for (const auto &receipt : receipts) {
-                const auto event_id = receipt.first;
-                auto event_receipts = receipt.second;
+    auto user_id = this->localUserId_.toStdString();
+    for (const auto &receipt : receipts) {
+        const auto event_id = receipt.first;
+        auto event_receipts = receipt.second;
 
-                ReadReceiptKey receipt_key{event_id, room_id};
-                nlohmann::json json_key = receipt_key;
+        ReadReceiptKey receipt_key{event_id, room_id};
+        nlohmann::json json_key = receipt_key;
 
-                try {
-                        const auto key = json_key.dump();
+        try {
+            const auto key = json_key.dump();
 
-                        std::string_view prev_value;
+            std::string_view prev_value;
 
-                        bool exists = readReceiptsDb_.get(txn, key, prev_value);
+            bool exists = readReceiptsDb_.get(txn, key, prev_value);
 
-                        std::map<std::string, uint64_t> saved_receipts;
+            std::map<std::string, uint64_t> saved_receipts;
 
-                        // If an entry for the event id already exists, we would
-                        // merge the existing receipts with the new ones.
-                        if (exists) {
-                                auto json_value = json::parse(
-                                  std::string_view(prev_value.data(), prev_value.size()));
+            // If an entry for the event id already exists, we would
+            // merge the existing receipts with the new ones.
+            if (exists) {
+                auto json_value =
+                  json::parse(std::string_view(prev_value.data(), prev_value.size()));
 
-                                // Retrieve the saved receipts.
-                                saved_receipts = json_value.get<std::map<std::string, uint64_t>>();
-                        }
+                // Retrieve the saved receipts.
+                saved_receipts = json_value.get<std::map<std::string, uint64_t>>();
+            }
 
-                        // Append the new ones.
-                        for (const auto &[read_by, timestamp] : event_receipts) {
-                                if (read_by == user_id) {
-                                        emit removeNotification(QString::fromStdString(room_id),
-                                                                QString::fromStdString(event_id));
-                                }
-                                saved_receipts.emplace(read_by, timestamp);
-                        }
+            // Append the new ones.
+            for (const auto &[read_by, timestamp] : event_receipts) {
+                if (read_by == user_id) {
+                    emit removeNotification(QString::fromStdString(room_id),
+                                            QString::fromStdString(event_id));
+                }
+                saved_receipts.emplace(read_by, timestamp);
+            }
 
-                        // Save back the merged (or only the new) receipts.
-                        nlohmann::json json_updated_value = saved_receipts;
-                        std::string merged_receipts       = json_updated_value.dump();
+            // Save back the merged (or only the new) receipts.
+            nlohmann::json json_updated_value = saved_receipts;
+            std::string merged_receipts       = json_updated_value.dump();
 
-                        readReceiptsDb_.put(txn, key, merged_receipts);
+            readReceiptsDb_.put(txn, key, merged_receipts);
 
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->critical("updateReadReceipts: {}", e.what());
-                }
+        } catch (const lmdb::error &e) {
+            nhlog::db()->critical("updateReadReceipts: {}", e.what());
         }
+    }
 }
 
 void
 Cache::calculateRoomReadStatus()
 {
-        const auto joined_rooms = joinedRooms();
+    const auto joined_rooms = joinedRooms();
 
-        std::map<QString, bool> readStatus;
+    std::map<QString, bool> readStatus;
 
-        for (const auto &room : joined_rooms)
-                readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room));
+    for (const auto &room : joined_rooms)
+        readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room));
 
-        emit roomReadStatus(readStatus);
+    emit roomReadStatus(readStatus);
 }
 
 bool
 Cache::calculateRoomReadStatus(const std::string &room_id)
 {
-        std::string last_event_id_, fullyReadEventId_;
-        {
-                auto txn = ro_txn(env_);
-
-                // Get last event id on the room.
-                const auto last_event_id = getLastEventId(txn, room_id);
-                const auto localUser     = utils::localUser().toStdString();
-
-                std::string fullyReadEventId;
-                if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) {
-                        if (auto fr = std::get_if<
-                              mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>(
-                              &ev.value())) {
-                                fullyReadEventId = fr->content.event_id;
-                        }
-                }
-
-                if (last_event_id.empty() || fullyReadEventId.empty())
-                        return true;
+    std::string last_event_id_, fullyReadEventId_;
+    {
+        auto txn = ro_txn(env_);
 
-                if (last_event_id == fullyReadEventId)
-                        return false;
+        // Get last event id on the room.
+        const auto last_event_id = getLastEventId(txn, room_id);
+        const auto localUser     = utils::localUser().toStdString();
 
-                last_event_id_    = std::string(last_event_id);
-                fullyReadEventId_ = std::string(fullyReadEventId);
+        std::string fullyReadEventId;
+        if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) {
+            if (auto fr =
+                  std::get_if<mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>(
+                    &ev.value())) {
+                fullyReadEventId = fr->content.event_id;
+            }
         }
 
-        // Retrieve all read receipts for that event.
-        return getEventIndex(room_id, last_event_id_) > getEventIndex(room_id, fullyReadEventId_);
+        if (last_event_id.empty() || fullyReadEventId.empty())
+            return true;
+
+        if (last_event_id == fullyReadEventId)
+            return false;
+
+        last_event_id_    = std::string(last_event_id);
+        fullyReadEventId_ = std::string(fullyReadEventId);
+    }
+
+    // Retrieve all read receipts for that event.
+    return getEventIndex(room_id, last_event_id_) > getEventIndex(room_id, fullyReadEventId_);
 }
 
 void
 Cache::saveState(const mtx::responses::Sync &res)
 {
-        using namespace mtx::events;
-        auto local_user_id = this->localUserId_.toStdString();
-
-        auto currentBatchToken = nextBatchToken();
-
-        auto txn = lmdb::txn::begin(env_);
-
-        setNextBatchToken(txn, res.next_batch);
-
-        if (!res.account_data.events.empty()) {
-                auto accountDataDb = getAccountDataDb(txn, "");
-                for (const auto &ev : res.account_data.events)
-                        std::visit(
-                          [&txn, &accountDataDb](const auto &event) {
-                                  auto j = json(event);
-                                  accountDataDb.put(txn, j["type"].get<std::string>(), j.dump());
-                          },
-                          ev);
-        }
+    using namespace mtx::events;
+    auto local_user_id = this->localUserId_.toStdString();
 
-        auto userKeyCacheDb = getUserKeysDb(txn);
-
-        std::set<std::string> spaces_with_updates;
-        std::set<std::string> rooms_with_space_updates;
-
-        // Save joined rooms
-        for (const auto &room : res.rooms.join) {
-                auto statesdb    = getStatesDb(txn, room.first);
-                auto stateskeydb = getStatesKeyDb(txn, room.first);
-                auto membersdb   = getMembersDb(txn, room.first);
-                auto eventsDb    = getEventsDb(txn, room.first);
-
-                saveStateEvents(txn,
-                                statesdb,
-                                stateskeydb,
-                                membersdb,
-                                eventsDb,
-                                room.first,
-                                room.second.state.events);
-                saveStateEvents(txn,
-                                statesdb,
-                                stateskeydb,
-                                membersdb,
-                                eventsDb,
-                                room.first,
-                                room.second.timeline.events);
-
-                saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline);
-
-                RoomInfo updatedInfo;
-                updatedInfo.name       = getRoomName(txn, statesdb, membersdb).toStdString();
-                updatedInfo.topic      = getRoomTopic(txn, statesdb).toStdString();
-                updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
-                updatedInfo.version    = getRoomVersion(txn, statesdb).toStdString();
-                updatedInfo.is_space   = getRoomIsSpace(txn, statesdb);
-
-                if (updatedInfo.is_space) {
-                        bool space_updates = false;
-                        for (const auto &e : room.second.state.events)
-                                if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
-                                    std::holds_alternative<StateEvent<state::PowerLevels>>(e))
-                                        space_updates = true;
-                        for (const auto &e : room.second.timeline.events)
-                                if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
-                                    std::holds_alternative<StateEvent<state::PowerLevels>>(e))
-                                        space_updates = true;
-
-                        if (space_updates)
-                                spaces_with_updates.insert(room.first);
-                }
-
-                {
-                        bool room_has_space_update = false;
-                        for (const auto &e : room.second.state.events) {
-                                if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
-                                        spaces_with_updates.insert(se->state_key);
-                                        room_has_space_update = true;
-                                }
-                        }
-                        for (const auto &e : room.second.timeline.events) {
-                                if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
-                                        spaces_with_updates.insert(se->state_key);
-                                        room_has_space_update = true;
-                                }
-                        }
+    auto currentBatchToken = nextBatchToken();
 
-                        if (room_has_space_update)
-                                rooms_with_space_updates.insert(room.first);
-                }
-
-                bool has_new_tags = false;
-                // Process the account_data associated with this room
-                if (!room.second.account_data.events.empty()) {
-                        auto accountDataDb = getAccountDataDb(txn, room.first);
-
-                        for (const auto &evt : room.second.account_data.events) {
-                                std::visit(
-                                  [&txn, &accountDataDb](const auto &event) {
-                                          auto j = json(event);
-                                          accountDataDb.put(
-                                            txn, j["type"].get<std::string>(), j.dump());
-                                  },
-                                  evt);
-
-                                // for tag events
-                                if (std::holds_alternative<AccountDataEvent<account_data::Tags>>(
-                                      evt)) {
-                                        auto tags_evt =
-                                          std::get<AccountDataEvent<account_data::Tags>>(evt);
-                                        has_new_tags = true;
-                                        for (const auto &tag : tags_evt.content.tags) {
-                                                updatedInfo.tags.push_back(tag.first);
-                                        }
-                                }
-                                if (auto fr = std::get_if<mtx::events::AccountDataEvent<
-                                      mtx::events::account_data::FullyRead>>(&evt)) {
-                                        nhlog::db()->debug("Fully read: {}", fr->content.event_id);
-                                }
-                        }
-                }
-                if (!has_new_tags) {
-                        // retrieve the old tags, they haven't changed
-                        std::string_view data;
-                        if (roomsDb_.get(txn, room.first, data)) {
-                                try {
-                                        RoomInfo tmp =
-                                          json::parse(std::string_view(data.data(), data.size()));
-                                        updatedInfo.tags = tmp.tags;
-                                } catch (const json::exception &e) {
-                                        nhlog::db()->warn(
-                                          "failed to parse room info: room_id ({}), {}: {}",
-                                          room.first,
-                                          std::string(data.data(), data.size()),
-                                          e.what());
-                                }
-                        }
-                }
+    auto txn = lmdb::txn::begin(env_);
 
-                roomsDb_.put(txn, room.first, json(updatedInfo).dump());
+    setNextBatchToken(txn, res.next_batch);
 
-                for (const auto &e : room.second.ephemeral.events) {
-                        if (auto receiptsEv = std::get_if<
-                              mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) {
-                                Receipts receipts;
+    if (!res.account_data.events.empty()) {
+        auto accountDataDb = getAccountDataDb(txn, "");
+        for (const auto &ev : res.account_data.events)
+            std::visit(
+              [&txn, &accountDataDb](const auto &event) {
+                  auto j = json(event);
+                  accountDataDb.put(txn, j["type"].get<std::string>(), j.dump());
+              },
+              ev);
+    }
 
-                                for (const auto &[event_id, userReceipts] :
-                                     receiptsEv->content.receipts) {
-                                        for (const auto &[user_id, receipt] : userReceipts.users) {
-                                                receipts[event_id][user_id] = receipt.ts;
-                                        }
-                                }
-                                updateReadReceipt(txn, room.first, receipts);
-                        }
-                }
+    auto userKeyCacheDb = getUserKeysDb(txn);
 
-                // Clean up non-valid invites.
-                removeInvite(txn, room.first);
-        }
+    std::set<std::string> spaces_with_updates;
+    std::set<std::string> rooms_with_space_updates;
 
-        saveInvites(txn, res.rooms.invite);
+    // Save joined rooms
+    for (const auto &room : res.rooms.join) {
+        auto statesdb    = getStatesDb(txn, room.first);
+        auto stateskeydb = getStatesKeyDb(txn, room.first);
+        auto membersdb   = getMembersDb(txn, room.first);
+        auto eventsDb    = getEventsDb(txn, room.first);
 
-        savePresence(txn, res.presence);
+        saveStateEvents(
+          txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.state.events);
+        saveStateEvents(
+          txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.timeline.events);
 
-        markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken);
-        deleteUserKeys(txn, userKeyCacheDb, res.device_lists.left);
+        saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline);
 
-        removeLeftRooms(txn, res.rooms.leave);
+        RoomInfo updatedInfo;
+        updatedInfo.name       = getRoomName(txn, statesdb, membersdb).toStdString();
+        updatedInfo.topic      = getRoomTopic(txn, statesdb).toStdString();
+        updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
+        updatedInfo.version    = getRoomVersion(txn, statesdb).toStdString();
+        updatedInfo.is_space   = getRoomIsSpace(txn, statesdb);
 
-        updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates));
+        if (updatedInfo.is_space) {
+            bool space_updates = false;
+            for (const auto &e : room.second.state.events)
+                if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
+                    std::holds_alternative<StateEvent<state::PowerLevels>>(e))
+                    space_updates = true;
+            for (const auto &e : room.second.timeline.events)
+                if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
+                    std::holds_alternative<StateEvent<state::PowerLevels>>(e))
+                    space_updates = true;
 
-        txn.commit();
+            if (space_updates)
+                spaces_with_updates.insert(room.first);
+        }
 
-        std::map<QString, bool> readStatus;
-
-        for (const auto &room : res.rooms.join) {
-                for (const auto &e : room.second.ephemeral.events) {
-                        if (auto receiptsEv = std::get_if<
-                              mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) {
-                                std::vector<QString> receipts;
-
-                                for (const auto &[event_id, userReceipts] :
-                                     receiptsEv->content.receipts) {
-                                        for (const auto &[user_id, receipt] : userReceipts.users) {
-                                                (void)receipt;
-
-                                                if (user_id != local_user_id) {
-                                                        receipts.push_back(
-                                                          QString::fromStdString(event_id));
-                                                        break;
-                                                }
-                                        }
-                                }
-                                if (!receipts.empty())
-                                        emit newReadReceipts(QString::fromStdString(room.first),
-                                                             receipts);
-                        }
+        {
+            bool room_has_space_update = false;
+            for (const auto &e : room.second.state.events) {
+                if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
+                    spaces_with_updates.insert(se->state_key);
+                    room_has_space_update = true;
+                }
+            }
+            for (const auto &e : room.second.timeline.events) {
+                if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
+                    spaces_with_updates.insert(se->state_key);
+                    room_has_space_update = true;
+                }
+            }
+
+            if (room_has_space_update)
+                rooms_with_space_updates.insert(room.first);
+        }
+
+        bool has_new_tags = false;
+        // Process the account_data associated with this room
+        if (!room.second.account_data.events.empty()) {
+            auto accountDataDb = getAccountDataDb(txn, room.first);
+
+            for (const auto &evt : room.second.account_data.events) {
+                std::visit(
+                  [&txn, &accountDataDb](const auto &event) {
+                      auto j = json(event);
+                      accountDataDb.put(txn, j["type"].get<std::string>(), j.dump());
+                  },
+                  evt);
+
+                // for tag events
+                if (std::holds_alternative<AccountDataEvent<account_data::Tags>>(evt)) {
+                    auto tags_evt = std::get<AccountDataEvent<account_data::Tags>>(evt);
+                    has_new_tags  = true;
+                    for (const auto &tag : tags_evt.content.tags) {
+                        updatedInfo.tags.push_back(tag.first);
+                    }
+                }
+                if (auto fr = std::get_if<
+                      mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>(&evt)) {
+                    nhlog::db()->debug("Fully read: {}", fr->content.event_id);
+                }
+            }
+        }
+        if (!has_new_tags) {
+            // retrieve the old tags, they haven't changed
+            std::string_view data;
+            if (roomsDb_.get(txn, room.first, data)) {
+                try {
+                    RoomInfo tmp     = json::parse(std::string_view(data.data(), data.size()));
+                    updatedInfo.tags = tmp.tags;
+                } catch (const json::exception &e) {
+                    nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
+                                      room.first,
+                                      std::string(data.data(), data.size()),
+                                      e.what());
+                }
+            }
+        }
+
+        roomsDb_.put(txn, room.first, json(updatedInfo).dump());
+
+        for (const auto &e : room.second.ephemeral.events) {
+            if (auto receiptsEv =
+                  std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) {
+                Receipts receipts;
+
+                for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) {
+                    for (const auto &[user_id, receipt] : userReceipts.users) {
+                        receipts[event_id][user_id] = receipt.ts;
+                    }
+                }
+                updateReadReceipt(txn, room.first, receipts);
+            }
+        }
+
+        // Clean up non-valid invites.
+        removeInvite(txn, room.first);
+    }
+
+    saveInvites(txn, res.rooms.invite);
+
+    savePresence(txn, res.presence);
+
+    markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken);
+
+    removeLeftRooms(txn, res.rooms.leave);
+
+    updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates));
+
+    txn.commit();
+
+    std::map<QString, bool> readStatus;
+
+    for (const auto &room : res.rooms.join) {
+        for (const auto &e : room.second.ephemeral.events) {
+            if (auto receiptsEv =
+                  std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) {
+                std::vector<QString> receipts;
+
+                for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) {
+                    for (const auto &[user_id, receipt] : userReceipts.users) {
+                        (void)receipt;
+
+                        if (user_id != local_user_id) {
+                            receipts.push_back(QString::fromStdString(event_id));
+                            break;
+                        }
+                    }
                 }
-                readStatus.emplace(QString::fromStdString(room.first),
-                                   calculateRoomReadStatus(room.first));
+                if (!receipts.empty())
+                    emit newReadReceipts(QString::fromStdString(room.first), receipts);
+            }
         }
+        readStatus.emplace(QString::fromStdString(room.first), calculateRoomReadStatus(room.first));
+    }
 
-        emit roomReadStatus(readStatus);
+    emit roomReadStatus(readStatus);
 }
 
 void
 Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::InvitedRoom> &rooms)
 {
-        for (const auto &room : rooms) {
-                auto statesdb  = getInviteStatesDb(txn, room.first);
-                auto membersdb = getInviteMembersDb(txn, room.first);
+    for (const auto &room : rooms) {
+        auto statesdb  = getInviteStatesDb(txn, room.first);
+        auto membersdb = getInviteMembersDb(txn, room.first);
 
-                saveInvite(txn, statesdb, membersdb, room.second);
+        saveInvite(txn, statesdb, membersdb, room.second);
 
-                RoomInfo updatedInfo;
-                updatedInfo.name  = getInviteRoomName(txn, statesdb, membersdb).toStdString();
-                updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString();
-                updatedInfo.avatar_url =
-                  getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
-                updatedInfo.is_space  = getInviteRoomIsSpace(txn, statesdb);
-                updatedInfo.is_invite = true;
+        RoomInfo updatedInfo;
+        updatedInfo.name       = getInviteRoomName(txn, statesdb, membersdb).toStdString();
+        updatedInfo.topic      = getInviteRoomTopic(txn, statesdb).toStdString();
+        updatedInfo.avatar_url = getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
+        updatedInfo.is_space   = getInviteRoomIsSpace(txn, statesdb);
+        updatedInfo.is_invite  = true;
 
-                invitesDb_.put(txn, room.first, json(updatedInfo).dump());
-        }
+        invitesDb_.put(txn, room.first, json(updatedInfo).dump());
+    }
 }
 
 void
@@ -1518,32 +1599,29 @@ Cache::saveInvite(lmdb::txn &txn,
                   lmdb::dbi &membersdb,
                   const mtx::responses::InvitedRoom &room)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        for (const auto &e : room.invite_state) {
-                if (auto msg = std::get_if<StrippedEvent<Member>>(&e)) {
-                        auto display_name = msg->content.display_name.empty()
-                                              ? msg->state_key
-                                              : msg->content.display_name;
+    for (const auto &e : room.invite_state) {
+        if (auto msg = std::get_if<StrippedEvent<Member>>(&e)) {
+            auto display_name =
+              msg->content.display_name.empty() ? msg->state_key : msg->content.display_name;
 
-                        MemberInfo tmp{display_name, msg->content.avatar_url};
+            MemberInfo tmp{display_name, msg->content.avatar_url};
 
-                        membersdb.put(txn, msg->state_key, json(tmp).dump());
-                } else {
-                        std::visit(
-                          [&txn, &statesdb](auto msg) {
-                                  auto j = json(msg);
-                                  bool res =
-                                    statesdb.put(txn, j["type"].get<std::string>(), j.dump());
-
-                                  if (!res)
-                                          nhlog::db()->warn("couldn't save data: {}",
-                                                            json(msg).dump());
-                          },
-                          e);
-                }
+            membersdb.put(txn, msg->state_key, json(tmp).dump());
+        } else {
+            std::visit(
+              [&txn, &statesdb](auto msg) {
+                  auto j   = json(msg);
+                  bool res = statesdb.put(txn, j["type"].get<std::string>(), j.dump());
+
+                  if (!res)
+                      nhlog::db()->warn("couldn't save data: {}", json(msg).dump());
+              },
+              e);
         }
+    }
 }
 
 void
@@ -1551,281 +1629,279 @@ Cache::savePresence(
   lmdb::txn &txn,
   const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates)
 {
-        for (const auto &update : presenceUpdates) {
-                auto presenceDb = getPresenceDb(txn);
+    for (const auto &update : presenceUpdates) {
+        auto presenceDb = getPresenceDb(txn);
 
-                presenceDb.put(txn, update.sender, json(update.content).dump());
-        }
+        presenceDb.put(txn, update.sender, json(update.content).dump());
+    }
 }
 
 std::vector<std::string>
 Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
 {
-        std::vector<std::string> rooms;
-        for (const auto &room : res.rooms.join) {
-                bool hasUpdates = false;
-                for (const auto &s : room.second.state.events) {
-                        if (containsStateUpdates(s)) {
-                                hasUpdates = true;
-                                break;
-                        }
-                }
-
-                for (const auto &s : room.second.timeline.events) {
-                        if (containsStateUpdates(s)) {
-                                hasUpdates = true;
-                                break;
-                        }
-                }
+    std::vector<std::string> rooms;
+    for (const auto &room : res.rooms.join) {
+        bool hasUpdates = false;
+        for (const auto &s : room.second.state.events) {
+            if (containsStateUpdates(s)) {
+                hasUpdates = true;
+                break;
+            }
+        }
 
-                if (hasUpdates)
-                        rooms.emplace_back(room.first);
+        for (const auto &s : room.second.timeline.events) {
+            if (containsStateUpdates(s)) {
+                hasUpdates = true;
+                break;
+            }
         }
 
-        for (const auto &room : res.rooms.invite) {
-                for (const auto &s : room.second.invite_state) {
-                        if (containsStateUpdates(s)) {
-                                rooms.emplace_back(room.first);
-                                break;
-                        }
-                }
+        if (hasUpdates)
+            rooms.emplace_back(room.first);
+    }
+
+    for (const auto &room : res.rooms.invite) {
+        for (const auto &s : room.second.invite_state) {
+            if (containsStateUpdates(s)) {
+                rooms.emplace_back(room.first);
+                break;
+            }
         }
+    }
 
-        return rooms;
+    return rooms;
 }
 
 RoomInfo
 Cache::singleRoomInfo(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        try {
-                auto statesdb = getStatesDb(txn, room_id);
-
-                std::string_view data;
-
-                // Check if the room is joined.
-                if (roomsDb_.get(txn, room_id, data)) {
-                        try {
-                                RoomInfo tmp     = json::parse(data);
-                                tmp.member_count = getMembersDb(txn, room_id).size(txn);
-                                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
-                                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
-
-                                return tmp;
-                        } catch (const json::exception &e) {
-                                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
-                                                  room_id,
-                                                  std::string(data.data(), data.size()),
-                                                  e.what());
-                        }
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn(
-                  "failed to read room info from db: room_id ({}), {}", room_id, e.what());
+    try {
+        auto statesdb = getStatesDb(txn, room_id);
+
+        std::string_view data;
+
+        // Check if the room is joined.
+        if (roomsDb_.get(txn, room_id, data)) {
+            try {
+                RoomInfo tmp     = json::parse(data);
+                tmp.member_count = getMembersDb(txn, room_id).size(txn);
+                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
+                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
+
+                return tmp;
+            } catch (const json::exception &e) {
+                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
+                                  room_id,
+                                  std::string(data.data(), data.size()),
+                                  e.what());
+            }
         }
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("failed to read room info from db: room_id ({}), {}", room_id, e.what());
+    }
 
-        return RoomInfo();
+    return RoomInfo();
 }
 
 std::map<QString, RoomInfo>
 Cache::getRoomInfo(const std::vector<std::string> &rooms)
 {
-        std::map<QString, RoomInfo> room_info;
+    std::map<QString, RoomInfo> room_info;
 
-        // TODO This should be read only.
-        auto txn = lmdb::txn::begin(env_);
+    // TODO This should be read only.
+    auto txn = lmdb::txn::begin(env_);
 
-        for (const auto &room : rooms) {
-                std::string_view data;
-                auto statesdb = getStatesDb(txn, room);
-
-                // Check if the room is joined.
-                if (roomsDb_.get(txn, room, data)) {
-                        try {
-                                RoomInfo tmp     = json::parse(data);
-                                tmp.member_count = getMembersDb(txn, room).size(txn);
-                                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
-                                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
-
-                                room_info.emplace(QString::fromStdString(room), std::move(tmp));
-                        } catch (const json::exception &e) {
-                                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
-                                                  room,
-                                                  std::string(data.data(), data.size()),
-                                                  e.what());
-                        }
-                } else {
-                        // Check if the room is an invite.
-                        if (invitesDb_.get(txn, room, data)) {
-                                try {
-                                        RoomInfo tmp     = json::parse(std::string_view(data));
-                                        tmp.member_count = getInviteMembersDb(txn, room).size(txn);
-
-                                        room_info.emplace(QString::fromStdString(room),
-                                                          std::move(tmp));
-                                } catch (const json::exception &e) {
-                                        nhlog::db()->warn("failed to parse room info for invite: "
-                                                          "room_id ({}), {}: {}",
-                                                          room,
-                                                          std::string(data.data(), data.size()),
-                                                          e.what());
-                                }
-                        }
+    for (const auto &room : rooms) {
+        std::string_view data;
+        auto statesdb = getStatesDb(txn, room);
+
+        // Check if the room is joined.
+        if (roomsDb_.get(txn, room, data)) {
+            try {
+                RoomInfo tmp     = json::parse(data);
+                tmp.member_count = getMembersDb(txn, room).size(txn);
+                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
+                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
+
+                room_info.emplace(QString::fromStdString(room), std::move(tmp));
+            } catch (const json::exception &e) {
+                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
+                                  room,
+                                  std::string(data.data(), data.size()),
+                                  e.what());
+            }
+        } else {
+            // Check if the room is an invite.
+            if (invitesDb_.get(txn, room, data)) {
+                try {
+                    RoomInfo tmp     = json::parse(std::string_view(data));
+                    tmp.member_count = getInviteMembersDb(txn, room).size(txn);
+
+                    room_info.emplace(QString::fromStdString(room), std::move(tmp));
+                } catch (const json::exception &e) {
+                    nhlog::db()->warn("failed to parse room info for invite: "
+                                      "room_id ({}), {}: {}",
+                                      room,
+                                      std::string(data.data(), data.size()),
+                                      e.what());
                 }
+            }
         }
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return room_info;
+    return room_info;
 }
 
 std::vector<QString>
 Cache::roomIds()
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector<QString> rooms;
-        std::string_view room_id, unused;
+    std::vector<QString> rooms;
+    std::string_view room_id, unused;
 
-        auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
-        while (roomsCursor.get(room_id, unused, MDB_NEXT))
-                rooms.push_back(QString::fromStdString(std::string(room_id)));
+    auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
+    while (roomsCursor.get(room_id, unused, MDB_NEXT))
+        rooms.push_back(QString::fromStdString(std::string(room_id)));
 
-        roomsCursor.close();
+    roomsCursor.close();
 
-        return rooms;
+    return rooms;
 }
 
 QMap<QString, mtx::responses::Notifications>
 Cache::getTimelineMentions()
 {
-        // TODO: Should be read-only, but getMentionsDb will attempt to create a DB
-        // if it doesn't exist, throwing an error.
-        auto txn = lmdb::txn::begin(env_, nullptr);
+    // TODO: Should be read-only, but getMentionsDb will attempt to create a DB
+    // if it doesn't exist, throwing an error.
+    auto txn = lmdb::txn::begin(env_, nullptr);
 
-        QMap<QString, mtx::responses::Notifications> notifs;
+    QMap<QString, mtx::responses::Notifications> notifs;
 
-        auto room_ids = getRoomIds(txn);
+    auto room_ids = getRoomIds(txn);
 
-        for (const auto &room_id : room_ids) {
-                auto roomNotifs                         = getTimelineMentionsForRoom(txn, room_id);
-                notifs[QString::fromStdString(room_id)] = roomNotifs;
-        }
+    for (const auto &room_id : room_ids) {
+        auto roomNotifs                         = getTimelineMentionsForRoom(txn, room_id);
+        notifs[QString::fromStdString(room_id)] = roomNotifs;
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return notifs;
+    return notifs;
 }
 
 std::string
 Cache::previousBatchToken(const std::string &room_id)
 {
-        auto txn     = lmdb::txn::begin(env_, nullptr);
-        auto orderDb = getEventOrderDb(txn, room_id);
+    auto txn     = lmdb::txn::begin(env_, nullptr);
+    auto orderDb = getEventOrderDb(txn, room_id);
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        std::string_view indexVal, val;
-        if (!cursor.get(indexVal, val, MDB_FIRST)) {
-                return "";
-        }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    std::string_view indexVal, val;
+    if (!cursor.get(indexVal, val, MDB_FIRST)) {
+        return "";
+    }
 
-        auto j = json::parse(val);
+    auto j = json::parse(val);
 
-        return j.value("prev_batch", "");
+    return j.value("prev_batch", "");
 }
 
 Cache::Messages
 Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t index, bool forward)
 {
-        // TODO(nico): Limit the messages returned by this maybe?
-        auto orderDb  = getOrderToMessageDb(txn, room_id);
-        auto eventsDb = getEventsDb(txn, room_id);
+    // TODO(nico): Limit the messages returned by this maybe?
+    auto orderDb  = getOrderToMessageDb(txn, room_id);
+    auto eventsDb = getEventsDb(txn, room_id);
 
-        Messages messages{};
+    Messages messages{};
 
-        std::string_view indexVal, event_id;
+    std::string_view indexVal, event_id;
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        if (index == std::numeric_limits<uint64_t>::max()) {
-                if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
-                        index = lmdb::from_sv<uint64_t>(indexVal);
-                } else {
-                        messages.end_of_cache = true;
-                        return messages;
-                }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    if (index == std::numeric_limits<uint64_t>::max()) {
+        if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
+            index = lmdb::from_sv<uint64_t>(indexVal);
         } else {
-                if (cursor.get(indexVal, event_id, MDB_SET)) {
-                        index = lmdb::from_sv<uint64_t>(indexVal);
-                } else {
-                        messages.end_of_cache = true;
-                        return messages;
-                }
+            messages.end_of_cache = true;
+            return messages;
         }
+    } else {
+        if (cursor.get(indexVal, event_id, MDB_SET)) {
+            index = lmdb::from_sv<uint64_t>(indexVal);
+        } else {
+            messages.end_of_cache = true;
+            return messages;
+        }
+    }
 
-        int counter = 0;
-
-        bool ret;
-        while ((ret = cursor.get(indexVal,
-                                 event_id,
-                                 counter == 0 ? (forward ? MDB_FIRST : MDB_LAST)
-                                              : (forward ? MDB_NEXT : MDB_PREV))) &&
-               counter++ < BATCH_SIZE) {
-                std::string_view event;
-                bool success = eventsDb.get(txn, event_id, event);
-                if (!success)
-                        continue;
+    int counter = 0;
 
-                mtx::events::collections::TimelineEvent te;
-                try {
-                        mtx::events::collections::from_json(json::parse(event), te);
-                } catch (std::exception &e) {
-                        nhlog::db()->error("Failed to parse message from cache {}", e.what());
-                        continue;
-                }
+    bool ret;
+    while ((ret = cursor.get(indexVal,
+                             event_id,
+                             counter == 0 ? (forward ? MDB_FIRST : MDB_LAST)
+                                          : (forward ? MDB_NEXT : MDB_PREV))) &&
+           counter++ < BATCH_SIZE) {
+        std::string_view event;
+        bool success = eventsDb.get(txn, event_id, event);
+        if (!success)
+            continue;
 
-                messages.timeline.events.push_back(std::move(te.data));
+        mtx::events::collections::TimelineEvent te;
+        try {
+            mtx::events::collections::from_json(json::parse(event), te);
+        } catch (std::exception &e) {
+            nhlog::db()->error("Failed to parse message from cache {}", e.what());
+            continue;
         }
-        cursor.close();
 
-        // std::reverse(timeline.events.begin(), timeline.events.end());
-        messages.next_index   = lmdb::from_sv<uint64_t>(indexVal);
-        messages.end_of_cache = !ret;
+        messages.timeline.events.push_back(std::move(te.data));
+    }
+    cursor.close();
+
+    // std::reverse(timeline.events.begin(), timeline.events.end());
+    messages.next_index   = lmdb::from_sv<uint64_t>(indexVal);
+    messages.end_of_cache = !ret;
 
-        return messages;
+    return messages;
 }
 
 std::optional<mtx::events::collections::TimelineEvent>
 Cache::getEvent(const std::string &room_id, const std::string &event_id)
 {
-        auto txn      = ro_txn(env_);
-        auto eventsDb = getEventsDb(txn, room_id);
+    auto txn      = ro_txn(env_);
+    auto eventsDb = getEventsDb(txn, room_id);
 
-        std::string_view event{};
-        bool success = eventsDb.get(txn, event_id, event);
-        if (!success)
-                return {};
+    std::string_view event{};
+    bool success = eventsDb.get(txn, event_id, event);
+    if (!success)
+        return {};
 
-        mtx::events::collections::TimelineEvent te;
-        try {
-                mtx::events::collections::from_json(json::parse(event), te);
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to parse message from cache {}", e.what());
-                return std::nullopt;
-        }
+    mtx::events::collections::TimelineEvent te;
+    try {
+        mtx::events::collections::from_json(json::parse(event), te);
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to parse message from cache {}", e.what());
+        return std::nullopt;
+    }
 
-        return te;
+    return te;
 }
 void
 Cache::storeEvent(const std::string &room_id,
                   const std::string &event_id,
                   const mtx::events::collections::TimelineEvent &event)
 {
-        auto txn        = lmdb::txn::begin(env_);
-        auto eventsDb   = getEventsDb(txn, room_id);
-        auto event_json = mtx::accessors::serialize_event(event.data);
-        eventsDb.put(txn, event_id, event_json.dump());
-        txn.commit();
+    auto txn        = lmdb::txn::begin(env_);
+    auto eventsDb   = getEventsDb(txn, room_id);
+    auto event_json = mtx::accessors::serialize_event(event.data);
+    eventsDb.put(txn, event_id, event_json.dump());
+    txn.commit();
 }
 
 void
@@ -1833,922 +1909,944 @@ Cache::replaceEvent(const std::string &room_id,
                     const std::string &event_id,
                     const mtx::events::collections::TimelineEvent &event)
 {
-        auto txn         = lmdb::txn::begin(env_);
-        auto eventsDb    = getEventsDb(txn, room_id);
-        auto relationsDb = getRelationsDb(txn, room_id);
-        auto event_json  = mtx::accessors::serialize_event(event.data).dump();
+    auto txn         = lmdb::txn::begin(env_);
+    auto eventsDb    = getEventsDb(txn, room_id);
+    auto relationsDb = getRelationsDb(txn, room_id);
+    auto event_json  = mtx::accessors::serialize_event(event.data).dump();
 
-        {
-                eventsDb.del(txn, event_id);
-                eventsDb.put(txn, event_id, event_json);
-                for (auto relation : mtx::accessors::relations(event.data).relations) {
-                        relationsDb.put(txn, relation.event_id, event_id);
-                }
+    {
+        eventsDb.del(txn, event_id);
+        eventsDb.put(txn, event_id, event_json);
+        for (auto relation : mtx::accessors::relations(event.data).relations) {
+            relationsDb.put(txn, relation.event_id, event_id);
         }
+    }
 
-        txn.commit();
+    txn.commit();
 }
 
 std::vector<std::string>
 Cache::relatedEvents(const std::string &room_id, const std::string &event_id)
 {
-        auto txn         = ro_txn(env_);
-        auto relationsDb = getRelationsDb(txn, room_id);
+    auto txn         = ro_txn(env_);
+    auto relationsDb = getRelationsDb(txn, room_id);
 
-        std::vector<std::string> related_ids;
+    std::vector<std::string> related_ids;
 
-        auto related_cursor         = lmdb::cursor::open(txn, relationsDb);
-        std::string_view related_to = event_id, related_event;
-        bool first                  = true;
+    auto related_cursor         = lmdb::cursor::open(txn, relationsDb);
+    std::string_view related_to = event_id, related_event;
+    bool first                  = true;
 
-        try {
-                if (!related_cursor.get(related_to, related_event, MDB_SET))
-                        return {};
+    try {
+        if (!related_cursor.get(related_to, related_event, MDB_SET))
+            return {};
 
-                while (related_cursor.get(
-                  related_to, related_event, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                        first = false;
-                        if (event_id != std::string_view(related_to.data(), related_to.size()))
-                                break;
+        while (
+          related_cursor.get(related_to, related_event, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+            first = false;
+            if (event_id != std::string_view(related_to.data(), related_to.size()))
+                break;
 
-                        related_ids.emplace_back(related_event.data(), related_event.size());
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("related events error: {}", e.what());
+            related_ids.emplace_back(related_event.data(), related_event.size());
         }
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("related events error: {}", e.what());
+    }
 
-        return related_ids;
+    return related_ids;
 }
 
 size_t
 Cache::memberCount(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
-        return getMembersDb(txn, room_id).size(txn);
+    auto txn = ro_txn(env_);
+    return getMembersDb(txn, room_id).size(txn);
 }
 
 QMap<QString, RoomInfo>
 Cache::roomInfo(bool withInvites)
 {
-        QMap<QString, RoomInfo> result;
+    QMap<QString, RoomInfo> result;
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::string_view room_id;
-        std::string_view room_data;
+    std::string_view room_id;
+    std::string_view room_data;
 
-        // Gather info about the joined rooms.
-        auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
-        while (roomsCursor.get(room_id, room_data, MDB_NEXT)) {
-                RoomInfo tmp     = json::parse(std::move(room_data));
-                tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn);
-                result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
-        }
-        roomsCursor.close();
-
-        if (withInvites) {
-                // Gather info about the invites.
-                auto invitesCursor = lmdb::cursor::open(txn, invitesDb_);
-                while (invitesCursor.get(room_id, room_data, MDB_NEXT)) {
-                        RoomInfo tmp     = json::parse(room_data);
-                        tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
-                        result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
-                }
-                invitesCursor.close();
+    // Gather info about the joined rooms.
+    auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
+    while (roomsCursor.get(room_id, room_data, MDB_NEXT)) {
+        RoomInfo tmp     = json::parse(std::move(room_data));
+        tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn);
+        result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
+    }
+    roomsCursor.close();
+
+    if (withInvites) {
+        // Gather info about the invites.
+        auto invitesCursor = lmdb::cursor::open(txn, invitesDb_);
+        while (invitesCursor.get(room_id, room_data, MDB_NEXT)) {
+            RoomInfo tmp     = json::parse(room_data);
+            tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
+            result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
         }
+        invitesCursor.close();
+    }
 
-        return result;
+    return result;
 }
 
 std::string
 Cache::getLastEventId(lmdb::txn &txn, const std::string &room_id)
 {
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getOrderToMessageDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getOrderToMessageDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view indexVal, val;
+    std::string_view indexVal, val;
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        if (!cursor.get(indexVal, val, MDB_LAST)) {
-                return {};
-        }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    if (!cursor.get(indexVal, val, MDB_LAST)) {
+        return {};
+    }
 
-        return std::string(val.data(), val.size());
+    return std::string(val.data(), val.size());
 }
 
 std::optional<Cache::TimelineRange>
 Cache::getTimelineRange(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getOrderToMessageDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    auto txn = ro_txn(env_);
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getOrderToMessageDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view indexVal, val;
+    std::string_view indexVal, val;
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        if (!cursor.get(indexVal, val, MDB_LAST)) {
-                return {};
-        }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    if (!cursor.get(indexVal, val, MDB_LAST)) {
+        return {};
+    }
 
-        TimelineRange range{};
-        range.last = lmdb::from_sv<uint64_t>(indexVal);
+    TimelineRange range{};
+    range.last = lmdb::from_sv<uint64_t>(indexVal);
 
-        if (!cursor.get(indexVal, val, MDB_FIRST)) {
-                return {};
-        }
-        range.first = lmdb::from_sv<uint64_t>(indexVal);
+    if (!cursor.get(indexVal, val, MDB_FIRST)) {
+        return {};
+    }
+    range.first = lmdb::from_sv<uint64_t>(indexVal);
 
-        return range;
+    return range;
 }
 std::optional<uint64_t>
 Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
 {
-        if (event_id.empty() || room_id.empty())
-                return {};
+    if (event_id.empty() || room_id.empty())
+        return {};
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getMessageToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getMessageToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view indexVal{event_id.data(), event_id.size()}, val;
+    std::string_view indexVal{event_id.data(), event_id.size()}, val;
 
-        bool success = orderDb.get(txn, indexVal, val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, indexVal, val);
+    if (!success) {
+        return {};
+    }
 
-        return lmdb::from_sv<uint64_t>(val);
+    return lmdb::from_sv<uint64_t>(val);
 }
 
 std::optional<uint64_t>
 Cache::getEventIndex(const std::string &room_id, std::string_view event_id)
 {
-        if (room_id.empty() || event_id.empty())
-                return {};
+    if (room_id.empty() || event_id.empty())
+        return {};
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getEventToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getEventToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view val;
+    std::string_view val;
 
-        bool success = orderDb.get(txn, event_id, val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, event_id, val);
+    if (!success) {
+        return {};
+    }
 
-        return lmdb::from_sv<uint64_t>(val);
+    return lmdb::from_sv<uint64_t>(val);
 }
 
 std::optional<std::pair<uint64_t, std::string>>
 Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id)
 {
-        if (room_id.empty() || event_id.empty())
-                return {};
-
-        auto txn = ro_txn(env_);
-
-        lmdb::dbi orderDb;
-        lmdb::dbi eventOrderDb;
-        lmdb::dbi timelineDb;
-        try {
-                orderDb      = getEventToOrderDb(txn, room_id);
-                eventOrderDb = getEventOrderDb(txn, room_id);
-                timelineDb   = getMessageToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
-
-        std::string_view indexVal;
-
-        bool success = orderDb.get(txn, event_id, indexVal);
-        if (!success) {
-                return {};
-        }
-
-        try {
-                uint64_t prevIdx = lmdb::from_sv<uint64_t>(indexVal);
-                std::string prevId{event_id};
-
-                auto cursor = lmdb::cursor::open(txn, eventOrderDb);
-                cursor.get(indexVal, MDB_SET);
-                while (cursor.get(indexVal, event_id, MDB_NEXT)) {
-                        std::string evId = json::parse(event_id)["event_id"].get<std::string>();
-                        std::string_view temp;
-                        if (timelineDb.get(txn, evId, temp)) {
-                                return std::pair{prevIdx, std::string(prevId)};
-                        } else {
-                                prevIdx = lmdb::from_sv<uint64_t>(indexVal);
-                                prevId  = std::move(evId);
-                        }
-                }
-
+    if (room_id.empty() || event_id.empty())
+        return {};
+
+    auto txn = ro_txn(env_);
+
+    lmdb::dbi orderDb;
+    lmdb::dbi eventOrderDb;
+    lmdb::dbi timelineDb;
+    try {
+        orderDb      = getEventToOrderDb(txn, room_id);
+        eventOrderDb = getEventOrderDb(txn, room_id);
+        timelineDb   = getMessageToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
+
+    std::string_view indexVal;
+
+    bool success = orderDb.get(txn, event_id, indexVal);
+    if (!success) {
+        return {};
+    }
+
+    try {
+        uint64_t prevIdx = lmdb::from_sv<uint64_t>(indexVal);
+        std::string prevId{event_id};
+
+        auto cursor = lmdb::cursor::open(txn, eventOrderDb);
+        cursor.get(indexVal, MDB_SET);
+        while (cursor.get(indexVal, event_id, MDB_NEXT)) {
+            std::string evId = json::parse(event_id)["event_id"].get<std::string>();
+            std::string_view temp;
+            if (timelineDb.get(txn, evId, temp)) {
                 return std::pair{prevIdx, std::string(prevId)};
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error(
-                  "Failed to get last invisible event after {}", event_id, e.what());
-                return {};
+            } else {
+                prevIdx = lmdb::from_sv<uint64_t>(indexVal);
+                prevId  = std::move(evId);
+            }
         }
+
+        return std::pair{prevIdx, std::string(prevId)};
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error("Failed to get last invisible event after {}", event_id, e.what());
+        return {};
+    }
 }
 
 std::optional<uint64_t>
 Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getEventToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getEventToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view val;
+    std::string_view val;
 
-        bool success = orderDb.get(txn, event_id, val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, event_id, val);
+    if (!success) {
+        return {};
+    }
 
-        return lmdb::from_sv<uint64_t>(val);
+    return lmdb::from_sv<uint64_t>(val);
 }
 
 std::optional<std::string>
 Cache::getTimelineEventId(const std::string &room_id, uint64_t index)
 {
-        auto txn = ro_txn(env_);
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getOrderToMessageDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    auto txn = ro_txn(env_);
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getOrderToMessageDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view val;
+    std::string_view val;
 
-        bool success = orderDb.get(txn, lmdb::to_sv(index), val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, lmdb::to_sv(index), val);
+    if (!success) {
+        return {};
+    }
 
-        return std::string(val);
+    return std::string(val);
 }
 
 QHash<QString, RoomInfo>
 Cache::invites()
 {
-        QHash<QString, RoomInfo> result;
+    QHash<QString, RoomInfo> result;
 
-        auto txn    = ro_txn(env_);
-        auto cursor = lmdb::cursor::open(txn, invitesDb_);
+    auto txn    = ro_txn(env_);
+    auto cursor = lmdb::cursor::open(txn, invitesDb_);
 
-        std::string_view room_id, room_data;
+    std::string_view room_id, room_data;
 
-        while (cursor.get(room_id, room_data, MDB_NEXT)) {
-                try {
-                        RoomInfo tmp     = json::parse(room_data);
-                        tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
-                        result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse room info for invite: "
-                                          "room_id ({}), {}: {}",
-                                          room_id,
-                                          std::string(room_data),
-                                          e.what());
-                }
+    while (cursor.get(room_id, room_data, MDB_NEXT)) {
+        try {
+            RoomInfo tmp     = json::parse(room_data);
+            tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
+            result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse room info for invite: "
+                              "room_id ({}), {}: {}",
+                              room_id,
+                              std::string(room_data),
+                              e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        return result;
+    return result;
 }
 
 std::optional<RoomInfo>
 Cache::invite(std::string_view roomid)
 {
-        std::optional<RoomInfo> result;
+    std::optional<RoomInfo> result;
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::string_view room_data;
+    std::string_view room_data;
 
-        if (invitesDb_.get(txn, roomid, room_data)) {
-                try {
-                        RoomInfo tmp     = json::parse(room_data);
-                        tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn);
-                        result           = std::move(tmp);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse room info for invite: "
-                                          "room_id ({}), {}: {}",
-                                          roomid,
-                                          std::string(room_data),
-                                          e.what());
-                }
+    if (invitesDb_.get(txn, roomid, room_data)) {
+        try {
+            RoomInfo tmp     = json::parse(room_data);
+            tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn);
+            result           = std::move(tmp);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse room info for invite: "
+                              "room_id ({}), {}: {}",
+                              roomid,
+                              std::string(room_data),
+                              e.what());
         }
+    }
 
-        return result;
+    return result;
 }
 
 QString
 Cache::getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
 
-        if (res) {
-                try {
-                        StateEvent<Avatar> msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    if (res) {
+        try {
+            StateEvent<Avatar> msg = json::parse(std::string_view(event.data(), event.size()));
 
-                        if (!msg.content.url.empty())
-                                return QString::fromStdString(msg.content.url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
-                }
+            if (!msg.content.url.empty())
+                return QString::fromStdString(msg.content.url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
         }
+    }
 
-        // We don't use an avatar for group chats.
-        if (membersdb.size(txn) > 2)
-                return QString();
+    // We don't use an avatar for group chats.
+    if (membersdb.size(txn) > 2)
+        return QString();
 
-        auto cursor = lmdb::cursor::open(txn, membersdb);
-        std::string_view user_id;
-        std::string_view member_data;
-        std::string fallback_url;
+    auto cursor = lmdb::cursor::open(txn, membersdb);
+    std::string_view user_id;
+    std::string_view member_data;
+    std::string fallback_url;
 
-        // Resolve avatar for 1-1 chats.
-        while (cursor.get(user_id, member_data, MDB_NEXT)) {
-                try {
-                        MemberInfo m = json::parse(member_data);
-                        if (user_id == localUserId_.toStdString()) {
-                                fallback_url = m.avatar_url;
-                                continue;
-                        }
+    // Resolve avatar for 1-1 chats.
+    while (cursor.get(user_id, member_data, MDB_NEXT)) {
+        try {
+            MemberInfo m = json::parse(member_data);
+            if (user_id == localUserId_.toStdString()) {
+                fallback_url = m.avatar_url;
+                continue;
+            }
 
-                        cursor.close();
-                        return QString::fromStdString(m.avatar_url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
+            cursor.close();
+            return QString::fromStdString(m.avatar_url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        // Default case when there is only one member.
-        return QString::fromStdString(fallback_url);
+    // Default case when there is only one member.
+    return QString::fromStdString(fallback_url);
 }
 
 QString
 Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
 
-        if (res) {
-                try {
-                        StateEvent<Name> msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    if (res) {
+        try {
+            StateEvent<Name> msg = json::parse(std::string_view(event.data(), event.size()));
 
-                        if (!msg.content.name.empty())
-                                return QString::fromStdString(msg.content.name);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
-                }
+            if (!msg.content.name.empty())
+                return QString::fromStdString(msg.content.name);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
         }
+    }
 
-        res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
+    res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
 
-        if (res) {
-                try {
-                        StateEvent<CanonicalAlias> msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    if (res) {
+        try {
+            StateEvent<CanonicalAlias> msg =
+              json::parse(std::string_view(event.data(), event.size()));
 
-                        if (!msg.content.alias.empty())
-                                return QString::fromStdString(msg.content.alias);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
-                                          e.what());
-                }
+            if (!msg.content.alias.empty())
+                return QString::fromStdString(msg.content.alias);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", e.what());
         }
+    }
 
-        auto cursor      = lmdb::cursor::open(txn, membersdb);
-        const auto total = membersdb.size(txn);
+    auto cursor      = lmdb::cursor::open(txn, membersdb);
+    const auto total = membersdb.size(txn);
 
-        std::size_t ii = 0;
-        std::string_view user_id;
-        std::string_view member_data;
-        std::map<std::string, MemberInfo> members;
+    std::size_t ii = 0;
+    std::string_view user_id;
+    std::string_view member_data;
+    std::map<std::string, MemberInfo> members;
 
-        while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) {
-                try {
-                        members.emplace(user_id, json::parse(member_data));
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
-
-                ii++;
+    while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) {
+        try {
+            members.emplace(user_id, json::parse(member_data));
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
 
-        cursor.close();
+        ii++;
+    }
 
-        if (total == 1 && !members.empty())
-                return QString::fromStdString(members.begin()->second.name);
+    cursor.close();
 
-        auto first_member = [&members, this]() {
-                for (const auto &m : members) {
-                        if (m.first != localUserId_.toStdString())
-                                return QString::fromStdString(m.second.name);
-                }
+    if (total == 1 && !members.empty())
+        return QString::fromStdString(members.begin()->second.name);
+
+    auto first_member = [&members, this]() {
+        for (const auto &m : members) {
+            if (m.first != localUserId_.toStdString())
+                return QString::fromStdString(m.second.name);
+        }
 
-                return localUserId_;
-        }();
+        return localUserId_;
+    }();
 
-        if (total == 2)
-                return first_member;
-        else if (total > 2)
-                return QString("%1 and %2 others").arg(first_member).arg(total - 1);
+    if (total == 2)
+        return first_member;
+    else if (total > 2)
+        return QString("%1 and %2 others").arg(first_member).arg(total - 1);
 
-        return "Empty Room";
+    return "Empty Room";
 }
 
 mtx::events::state::JoinRule
 Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomJoinRules), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomJoinRules), event);
 
-        if (res) {
-                try {
-                        StateEvent<state::JoinRules> msg = json::parse(event);
-                        return msg.content.join_rule;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StateEvent<state::JoinRules> msg = json::parse(event);
+            return msg.content.join_rule;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
         }
-        return state::JoinRule::Knock;
+    }
+    return state::JoinRule::Knock;
 }
 
 bool
 Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomGuestAccess), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomGuestAccess), event);
 
-        if (res) {
-                try {
-                        StateEvent<GuestAccess> msg = json::parse(event);
-                        return msg.content.guest_access == AccessState::CanJoin;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.guest_access event: {}",
-                                          e.what());
-                }
+    if (res) {
+        try {
+            StateEvent<GuestAccess> msg = json::parse(event);
+            return msg.content.guest_access == AccessState::CanJoin;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.guest_access event: {}", e.what());
         }
-        return false;
+    }
+    return false;
 }
 
 QString
 Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
 
-        if (res) {
-                try {
-                        StateEvent<Topic> msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent<Topic> msg = json::parse(event);
 
-                        if (!msg.content.topic.empty())
-                                return QString::fromStdString(msg.content.topic);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
-                }
+            if (!msg.content.topic.empty())
+                return QString::fromStdString(msg.content.topic);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
         }
+    }
 
-        return QString();
+    return QString();
 }
 
 QString
 Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
 
-        if (res) {
-                try {
-                        StateEvent<Create> msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent<Create> msg = json::parse(event);
 
-                        if (!msg.content.room_version.empty())
-                                return QString::fromStdString(msg.content.room_version);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
-                }
+            if (!msg.content.room_version.empty())
+                return QString::fromStdString(msg.content.room_version);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
         }
+    }
 
-        nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
-        return QString("1");
+    nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
+    return QString("1");
 }
 
 bool
 Cache::getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
 
-        if (res) {
-                try {
-                        StateEvent<Create> msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent<Create> msg = json::parse(event);
 
-                        return msg.content.type == mtx::events::state::room_type::space;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
-                }
+            return msg.content.type == mtx::events::state::room_type::space;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
         }
+    }
 
-        nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
-        return false;
+    nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
+    return false;
 }
 
 std::optional<mtx::events::state::CanonicalAlias>
 Cache::getRoomAliases(const std::string &roomid)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        auto txn      = ro_txn(env_);
-        auto statesdb = getStatesDb(txn, roomid);
+    auto txn      = ro_txn(env_);
+    auto statesdb = getStatesDb(txn, roomid);
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
 
-        if (res) {
-                try {
-                        StateEvent<CanonicalAlias> msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent<CanonicalAlias> msg = json::parse(event);
 
-                        return msg.content;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
-                                          e.what());
-                }
+            return msg.content;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", e.what());
         }
+    }
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 QString
 Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
 
-        if (res) {
-                try {
-                        StrippedEvent<state::Name> msg = json::parse(event);
-                        return QString::fromStdString(msg.content.name);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent<state::Name> msg = json::parse(event);
+            return QString::fromStdString(msg.content.name);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
         }
+    }
 
-        auto cursor = lmdb::cursor::open(txn, membersdb);
-        std::string_view user_id, member_data;
+    auto cursor = lmdb::cursor::open(txn, membersdb);
+    std::string_view user_id, member_data;
 
-        while (cursor.get(user_id, member_data, MDB_NEXT)) {
-                if (user_id == localUserId_.toStdString())
-                        continue;
+    while (cursor.get(user_id, member_data, MDB_NEXT)) {
+        if (user_id == localUserId_.toStdString())
+            continue;
 
-                try {
-                        MemberInfo tmp = json::parse(member_data);
-                        cursor.close();
+        try {
+            MemberInfo tmp = json::parse(member_data);
+            cursor.close();
 
-                        return QString::fromStdString(tmp.name);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
+            return QString::fromStdString(tmp.name);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        return QString("Empty Room");
+    return QString("Empty Room");
 }
 
 QString
 Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
 
-        if (res) {
-                try {
-                        StrippedEvent<state::Avatar> msg = json::parse(event);
-                        return QString::fromStdString(msg.content.url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent<state::Avatar> msg = json::parse(event);
+            return QString::fromStdString(msg.content.url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
         }
+    }
 
-        auto cursor = lmdb::cursor::open(txn, membersdb);
-        std::string_view user_id, member_data;
+    auto cursor = lmdb::cursor::open(txn, membersdb);
+    std::string_view user_id, member_data;
 
-        while (cursor.get(user_id, member_data, MDB_NEXT)) {
-                if (user_id == localUserId_.toStdString())
-                        continue;
+    while (cursor.get(user_id, member_data, MDB_NEXT)) {
+        if (user_id == localUserId_.toStdString())
+            continue;
 
-                try {
-                        MemberInfo tmp = json::parse(member_data);
-                        cursor.close();
+        try {
+            MemberInfo tmp = json::parse(member_data);
+            cursor.close();
 
-                        return QString::fromStdString(tmp.avatar_url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
+            return QString::fromStdString(tmp.avatar_url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        return QString();
+    return QString();
 }
 
 QString
 Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = db.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
+    std::string_view event;
+    bool res = db.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
 
-        if (res) {
-                try {
-                        StrippedEvent<Topic> msg = json::parse(event);
-                        return QString::fromStdString(msg.content.topic);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent<Topic> msg = json::parse(event);
+            return QString::fromStdString(msg.content.topic);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
         }
+    }
 
-        return QString();
+    return QString();
 }
 
 bool
 Cache::getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+    std::string_view event;
+    bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
 
-        if (res) {
-                try {
-                        StrippedEvent<Create> msg = json::parse(event);
-                        return msg.content.type == mtx::events::state::room_type::space;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent<Create> msg = json::parse(event);
+            return msg.content.type == mtx::events::state::room_type::space;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
         }
+    }
 
-        return false;
+    return false;
 }
 
 std::vector<std::string>
 Cache::joinedRooms()
 {
-        auto txn         = ro_txn(env_);
-        auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
+    auto txn         = ro_txn(env_);
+    auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
 
-        std::string_view id, data;
-        std::vector<std::string> room_ids;
+    std::string_view id, data;
+    std::vector<std::string> room_ids;
 
-        // Gather the room ids for the joined rooms.
-        while (roomsCursor.get(id, data, MDB_NEXT))
-                room_ids.emplace_back(id);
+    // Gather the room ids for the joined rooms.
+    while (roomsCursor.get(id, data, MDB_NEXT))
+        room_ids.emplace_back(id);
 
-        roomsCursor.close();
+    roomsCursor.close();
 
-        return room_ids;
+    return room_ids;
 }
 
 std::optional<MemberInfo>
 Cache::getMember(const std::string &room_id, const std::string &user_id)
 {
-        if (user_id.empty() || !env_.handle())
-                return std::nullopt;
+    if (user_id.empty() || !env_.handle())
+        return std::nullopt;
 
-        try {
-                auto txn = ro_txn(env_);
+    try {
+        auto txn = ro_txn(env_);
 
-                auto membersdb = getMembersDb(txn, room_id);
+        auto membersdb = getMembersDb(txn, room_id);
 
-                std::string_view info;
-                if (membersdb.get(txn, user_id, info)) {
-                        MemberInfo m = json::parse(info);
-                        return m;
-                }
-        } catch (std::exception &e) {
-                nhlog::db()->warn(
-                  "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what());
+        std::string_view info;
+        if (membersdb.get(txn, user_id, info)) {
+            MemberInfo m = json::parse(info);
+            return m;
         }
-        return std::nullopt;
+    } catch (std::exception &e) {
+        nhlog::db()->warn(
+          "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what());
+    }
+    return std::nullopt;
 }
 
 std::vector<RoomMember>
 Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
 {
-        auto txn    = ro_txn(env_);
-        auto db     = getMembersDb(txn, room_id);
-        auto cursor = lmdb::cursor::open(txn, db);
+    auto txn    = ro_txn(env_);
+    auto db     = getMembersDb(txn, room_id);
+    auto cursor = lmdb::cursor::open(txn, db);
 
-        std::size_t currentIndex = 0;
+    std::size_t currentIndex = 0;
 
-        const auto endIndex = std::min(startIndex + len, db.size(txn));
+    const auto endIndex = std::min(startIndex + len, db.size(txn));
 
-        std::vector<RoomMember> members;
+    std::vector<RoomMember> members;
 
-        std::string_view user_id, user_data;
-        while (cursor.get(user_id, user_data, MDB_NEXT)) {
-                if (currentIndex < startIndex) {
-                        currentIndex += 1;
-                        continue;
-                }
+    std::string_view user_id, user_data;
+    while (cursor.get(user_id, user_data, MDB_NEXT)) {
+        if (currentIndex < startIndex) {
+            currentIndex += 1;
+            continue;
+        }
 
-                if (currentIndex >= endIndex)
-                        break;
+        if (currentIndex >= endIndex)
+            break;
 
-                try {
-                        MemberInfo tmp = json::parse(user_data);
-                        members.emplace_back(
-                          RoomMember{QString::fromStdString(std::string(user_id)),
-                                     QString::fromStdString(tmp.name)});
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("{}", e.what());
-                }
+        try {
+            MemberInfo tmp = json::parse(user_data);
+            members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)),
+                                            QString::fromStdString(tmp.name)});
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("{}", e.what());
+        }
+
+        currentIndex += 1;
+    }
+
+    cursor.close();
+
+    return members;
+}
+
+std::vector<RoomMember>
+Cache::getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len)
+{
+    auto txn    = ro_txn(env_);
+    auto db     = getInviteMembersDb(txn, room_id);
+    auto cursor = lmdb::cursor::open(txn, db);
+
+    std::size_t currentIndex = 0;
+
+    const auto endIndex = std::min(startIndex + len, db.size(txn));
 
-                currentIndex += 1;
+    std::vector<RoomMember> members;
+
+    std::string_view user_id, user_data;
+    while (cursor.get(user_id, user_data, MDB_NEXT)) {
+        if (currentIndex < startIndex) {
+            currentIndex += 1;
+            continue;
         }
 
-        cursor.close();
+        if (currentIndex >= endIndex)
+            break;
 
-        return members;
+        try {
+            MemberInfo tmp = json::parse(user_data);
+            members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)),
+                                            QString::fromStdString(tmp.name)});
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("{}", e.what());
+        }
+
+        currentIndex += 1;
+    }
+
+    cursor.close();
+
+    return members;
 }
 
 bool
 Cache::isRoomMember(const std::string &user_id, const std::string &room_id)
 {
-        try {
-                auto txn = ro_txn(env_);
-                auto db  = getMembersDb(txn, room_id);
+    try {
+        auto txn = ro_txn(env_);
+        auto db  = getMembersDb(txn, room_id);
 
-                std::string_view value;
-                bool res = db.get(txn, user_id, value);
+        std::string_view value;
+        bool res = db.get(txn, user_id, value);
 
-                return res;
-        } catch (std::exception &e) {
-                nhlog::db()->warn("Failed to read member membership ({}) in room ({}): {}",
-                                  user_id,
-                                  room_id,
-                                  e.what());
-        }
-        return false;
+        return res;
+    } catch (std::exception &e) {
+        nhlog::db()->warn(
+          "Failed to read member membership ({}) in room ({}): {}", user_id, room_id, e.what());
+    }
+    return false;
 }
 
 void
 Cache::savePendingMessage(const std::string &room_id,
                           const mtx::events::collections::TimelineEvent &message)
 {
-        auto txn      = lmdb::txn::begin(env_);
-        auto eventsDb = getEventsDb(txn, room_id);
+    auto txn      = lmdb::txn::begin(env_);
+    auto eventsDb = getEventsDb(txn, room_id);
 
-        mtx::responses::Timeline timeline;
-        timeline.events.push_back(message.data);
-        saveTimelineMessages(txn, eventsDb, room_id, timeline);
+    mtx::responses::Timeline timeline;
+    timeline.events.push_back(message.data);
+    saveTimelineMessages(txn, eventsDb, room_id, timeline);
 
-        auto pending = getPendingMessagesDb(txn, room_id);
+    auto pending = getPendingMessagesDb(txn, room_id);
 
-        int64_t now = QDateTime::currentMSecsSinceEpoch();
-        pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message.data));
+    int64_t now = QDateTime::currentMSecsSinceEpoch();
+    pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message.data));
 
-        txn.commit();
+    txn.commit();
 }
 
 std::optional<mtx::events::collections::TimelineEvent>
 Cache::firstPendingMessage(const std::string &room_id)
 {
-        auto txn     = lmdb::txn::begin(env_);
-        auto pending = getPendingMessagesDb(txn, room_id);
-
-        {
-                auto pendingCursor = lmdb::cursor::open(txn, pending);
-                std::string_view tsIgnored, pendingTxn;
-                while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
-                        auto eventsDb = getEventsDb(txn, room_id);
-                        std::string_view event;
-                        if (!eventsDb.get(txn, pendingTxn, event)) {
-                                pending.del(txn, tsIgnored, pendingTxn);
-                                continue;
-                        }
+    auto txn     = lmdb::txn::begin(env_);
+    auto pending = getPendingMessagesDb(txn, room_id);
+
+    {
+        auto pendingCursor = lmdb::cursor::open(txn, pending);
+        std::string_view tsIgnored, pendingTxn;
+        while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+            auto eventsDb = getEventsDb(txn, room_id);
+            std::string_view event;
+            if (!eventsDb.get(txn, pendingTxn, event)) {
+                pending.del(txn, tsIgnored, pendingTxn);
+                continue;
+            }
+
+            try {
+                mtx::events::collections::TimelineEvent te;
+                mtx::events::collections::from_json(json::parse(event), te);
 
-                        try {
-                                mtx::events::collections::TimelineEvent te;
-                                mtx::events::collections::from_json(json::parse(event), te);
-
-                                pendingCursor.close();
-                                txn.commit();
-                                return te;
-                        } catch (std::exception &e) {
-                                nhlog::db()->error("Failed to parse message from cache {}",
-                                                   e.what());
-                                pending.del(txn, tsIgnored, pendingTxn);
-                                continue;
-                        }
-                }
+                pendingCursor.close();
+                txn.commit();
+                return te;
+            } catch (std::exception &e) {
+                nhlog::db()->error("Failed to parse message from cache {}", e.what());
+                pending.del(txn, tsIgnored, pendingTxn);
+                continue;
+            }
         }
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 void
 Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id)
 {
-        auto txn     = lmdb::txn::begin(env_);
-        auto pending = getPendingMessagesDb(txn, room_id);
+    auto txn     = lmdb::txn::begin(env_);
+    auto pending = getPendingMessagesDb(txn, room_id);
 
-        {
-                auto pendingCursor = lmdb::cursor::open(txn, pending);
-                std::string_view tsIgnored, pendingTxn;
-                while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
-                        if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
-                                lmdb::cursor_del(pendingCursor);
-                }
+    {
+        auto pendingCursor = lmdb::cursor::open(txn, pending);
+        std::string_view tsIgnored, pendingTxn;
+        while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+            if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
+                lmdb::cursor_del(pendingCursor);
         }
+    }
 
-        txn.commit();
+    txn.commit();
 }
 
 void
@@ -2757,390 +2855,398 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
                             const std::string &room_id,
                             const mtx::responses::Timeline &res)
 {
-        if (res.events.empty())
-                return;
-
-        auto relationsDb = getRelationsDb(txn, room_id);
-
-        auto orderDb     = getEventOrderDb(txn, room_id);
-        auto evToOrderDb = getEventToOrderDb(txn, room_id);
-        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
-        auto order2msgDb = getOrderToMessageDb(txn, room_id);
-        auto pending     = getPendingMessagesDb(txn, room_id);
-
-        if (res.limited) {
-                lmdb::dbi_drop(txn, orderDb, false);
-                lmdb::dbi_drop(txn, evToOrderDb, false);
-                lmdb::dbi_drop(txn, msg2orderDb, false);
-                lmdb::dbi_drop(txn, order2msgDb, false);
-                lmdb::dbi_drop(txn, pending, true);
-        }
-
-        using namespace mtx::events;
-        using namespace mtx::events::state;
-
-        std::string_view indexVal, val;
-        uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
-        auto cursor    = lmdb::cursor::open(txn, orderDb);
-        if (cursor.get(indexVal, val, MDB_LAST)) {
-                index = lmdb::from_sv<uint64_t>(indexVal);
-        }
-
-        uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
-        auto msgCursor    = lmdb::cursor::open(txn, order2msgDb);
-        if (msgCursor.get(indexVal, val, MDB_LAST)) {
-                msgIndex = lmdb::from_sv<uint64_t>(indexVal);
-        }
-
-        bool first = true;
-        for (const auto &e : res.events) {
-                auto event  = mtx::accessors::serialize_event(e);
-                auto txn_id = mtx::accessors::transaction_id(e);
-
-                std::string event_id_val = event.value("event_id", "");
-                if (event_id_val.empty()) {
-                        nhlog::db()->error("Event without id!");
-                        continue;
+    if (res.events.empty())
+        return;
+
+    auto relationsDb = getRelationsDb(txn, room_id);
+
+    auto orderDb     = getEventOrderDb(txn, room_id);
+    auto evToOrderDb = getEventToOrderDb(txn, room_id);
+    auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+    auto order2msgDb = getOrderToMessageDb(txn, room_id);
+    auto pending     = getPendingMessagesDb(txn, room_id);
+
+    if (res.limited) {
+        lmdb::dbi_drop(txn, orderDb, false);
+        lmdb::dbi_drop(txn, evToOrderDb, false);
+        lmdb::dbi_drop(txn, msg2orderDb, false);
+        lmdb::dbi_drop(txn, order2msgDb, false);
+        lmdb::dbi_drop(txn, pending, true);
+    }
+
+    using namespace mtx::events;
+    using namespace mtx::events::state;
+
+    std::string_view indexVal, val;
+    uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
+    auto cursor    = lmdb::cursor::open(txn, orderDb);
+    if (cursor.get(indexVal, val, MDB_LAST)) {
+        index = lmdb::from_sv<uint64_t>(indexVal);
+    }
+
+    uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
+    auto msgCursor    = lmdb::cursor::open(txn, order2msgDb);
+    if (msgCursor.get(indexVal, val, MDB_LAST)) {
+        msgIndex = lmdb::from_sv<uint64_t>(indexVal);
+    }
+
+    bool first = true;
+    for (const auto &e : res.events) {
+        auto event  = mtx::accessors::serialize_event(e);
+        auto txn_id = mtx::accessors::transaction_id(e);
+
+        std::string event_id_val = event.value("event_id", "");
+        if (event_id_val.empty()) {
+            nhlog::db()->error("Event without id!");
+            continue;
+        }
+
+        std::string_view event_id = event_id_val;
+
+        json orderEntry        = json::object();
+        orderEntry["event_id"] = event_id_val;
+        if (first && !res.prev_batch.empty())
+            orderEntry["prev_batch"] = res.prev_batch;
+
+        std::string_view txn_order;
+        if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) {
+            eventsDb.put(txn, event_id, event.dump());
+            eventsDb.del(txn, txn_id);
+
+            std::string_view msg_txn_order;
+            if (msg2orderDb.get(txn, txn_id, msg_txn_order)) {
+                order2msgDb.put(txn, msg_txn_order, event_id);
+                msg2orderDb.put(txn, event_id, msg_txn_order);
+                msg2orderDb.del(txn, txn_id);
+            }
+
+            orderDb.put(txn, txn_order, orderEntry.dump());
+            evToOrderDb.put(txn, event_id, txn_order);
+            evToOrderDb.del(txn, txn_id);
+
+            auto relations = mtx::accessors::relations(e);
+            if (!relations.relations.empty()) {
+                for (const auto &r : relations.relations) {
+                    if (!r.event_id.empty()) {
+                        relationsDb.del(txn, r.event_id, txn_id);
+                        relationsDb.put(txn, r.event_id, event_id);
+                    }
                 }
+            }
+
+            auto pendingCursor = lmdb::cursor::open(txn, pending);
+            std::string_view tsIgnored, pendingTxn;
+            while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+                if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
+                    lmdb::cursor_del(pendingCursor);
+            }
+        } else if (auto redaction =
+                     std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
+            if (redaction->redacts.empty())
+                continue;
+
+            std::string_view oldEvent;
+            bool success = eventsDb.get(txn, redaction->redacts, oldEvent);
+            if (!success)
+                continue;
+
+            mtx::events::collections::TimelineEvent te;
+            try {
+                mtx::events::collections::from_json(
+                  json::parse(std::string_view(oldEvent.data(), oldEvent.size())), te);
+                // overwrite the content and add redation data
+                std::visit(
+                  [redaction](auto &ev) {
+                      ev.unsigned_data.redacted_because = *redaction;
+                      ev.unsigned_data.redacted_by      = redaction->event_id;
+                  },
+                  te.data);
+                event = mtx::accessors::serialize_event(te.data);
+                event["content"].clear();
+
+            } catch (std::exception &e) {
+                nhlog::db()->error("Failed to parse message from cache {}", e.what());
+                continue;
+            }
 
-                std::string_view event_id = event_id_val;
-
-                json orderEntry        = json::object();
-                orderEntry["event_id"] = event_id_val;
-                if (first && !res.prev_batch.empty())
-                        orderEntry["prev_batch"] = res.prev_batch;
-
-                std::string_view txn_order;
-                if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) {
-                        eventsDb.put(txn, event_id, event.dump());
-                        eventsDb.del(txn, txn_id);
-
-                        std::string_view msg_txn_order;
-                        if (msg2orderDb.get(txn, txn_id, msg_txn_order)) {
-                                order2msgDb.put(txn, msg_txn_order, event_id);
-                                msg2orderDb.put(txn, event_id, msg_txn_order);
-                                msg2orderDb.del(txn, txn_id);
-                        }
-
-                        orderDb.put(txn, txn_order, orderEntry.dump());
-                        evToOrderDb.put(txn, event_id, txn_order);
-                        evToOrderDb.del(txn, txn_id);
-
-                        auto relations = mtx::accessors::relations(e);
-                        if (!relations.relations.empty()) {
-                                for (const auto &r : relations.relations) {
-                                        if (!r.event_id.empty()) {
-                                                relationsDb.del(txn, r.event_id, txn_id);
-                                                relationsDb.put(txn, r.event_id, event_id);
-                                        }
-                                }
-                        }
-
-                        auto pendingCursor = lmdb::cursor::open(txn, pending);
-                        std::string_view tsIgnored, pendingTxn;
-                        while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
-                                if (std::string_view(pendingTxn.data(), pendingTxn.size()) ==
-                                    txn_id)
-                                        lmdb::cursor_del(pendingCursor);
-                        }
-                } else if (auto redaction =
-                             std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(
-                               &e)) {
-                        if (redaction->redacts.empty())
-                                continue;
-
-                        std::string_view oldEvent;
-                        bool success = eventsDb.get(txn, redaction->redacts, oldEvent);
-                        if (!success)
-                                continue;
-
-                        mtx::events::collections::TimelineEvent te;
-                        try {
-                                mtx::events::collections::from_json(
-                                  json::parse(std::string_view(oldEvent.data(), oldEvent.size())),
-                                  te);
-                                // overwrite the content and add redation data
-                                std::visit(
-                                  [redaction](auto &ev) {
-                                          ev.unsigned_data.redacted_because = *redaction;
-                                          ev.unsigned_data.redacted_by      = redaction->event_id;
-                                  },
-                                  te.data);
-                                event = mtx::accessors::serialize_event(te.data);
-                                event["content"].clear();
-
-                        } catch (std::exception &e) {
-                                nhlog::db()->error("Failed to parse message from cache {}",
-                                                   e.what());
-                                continue;
-                        }
-
-                        eventsDb.put(txn, redaction->redacts, event.dump());
-                        eventsDb.put(txn, redaction->event_id, json(*redaction).dump());
-                } else {
-                        eventsDb.put(txn, event_id, event.dump());
-
-                        ++index;
-
-                        first = false;
+            eventsDb.put(txn, redaction->redacts, event.dump());
+            eventsDb.put(txn, redaction->event_id, json(*redaction).dump());
+        } else {
+            first = false;
 
-                        nhlog::db()->debug("saving '{}'", orderEntry.dump());
+            // This check protects against duplicates in the timeline. If the event_id
+            // is already in the DB, we skip putting it (again) in ordered DBs, and only
+            // update the event itself and its relations.
+            std::string_view unused_read;
+            if (!evToOrderDb.get(txn, event_id, unused_read)) {
+                ++index;
 
-                        cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND);
-                        evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
+                nhlog::db()->debug("saving '{}'", orderEntry.dump());
 
-                        // TODO(Nico): Allow blacklisting more event types in UI
-                        if (!isHiddenEvent(txn, e, room_id)) {
-                                ++msgIndex;
-                                msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND);
+                cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND);
+                evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
 
-                                msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
-                        }
+                // TODO(Nico): Allow blacklisting more event types in UI
+                if (!isHiddenEvent(txn, e, room_id)) {
+                    ++msgIndex;
+                    msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND);
 
-                        auto relations = mtx::accessors::relations(e);
-                        if (!relations.relations.empty()) {
-                                for (const auto &r : relations.relations) {
-                                        if (!r.event_id.empty()) {
-                                                relationsDb.put(txn, r.event_id, event_id);
-                                        }
-                                }
-                        }
+                    msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
+                }
+            } else {
+                nhlog::db()->warn("duplicate event '{}'", orderEntry.dump());
+            }
+            eventsDb.put(txn, event_id, event.dump());
+
+            auto relations = mtx::accessors::relations(e);
+            if (!relations.relations.empty()) {
+                for (const auto &r : relations.relations) {
+                    if (!r.event_id.empty()) {
+                        relationsDb.put(txn, r.event_id, event_id);
+                    }
                 }
+            }
         }
+    }
 }
 
 uint64_t
 Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res)
 {
-        auto txn         = lmdb::txn::begin(env_);
-        auto eventsDb    = getEventsDb(txn, room_id);
-        auto relationsDb = getRelationsDb(txn, room_id);
+    auto txn         = lmdb::txn::begin(env_);
+    auto eventsDb    = getEventsDb(txn, room_id);
+    auto relationsDb = getRelationsDb(txn, room_id);
 
-        auto orderDb     = getEventOrderDb(txn, room_id);
-        auto evToOrderDb = getEventToOrderDb(txn, room_id);
-        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
-        auto order2msgDb = getOrderToMessageDb(txn, room_id);
-
-        std::string_view indexVal, val;
-        uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
-        {
-                auto cursor = lmdb::cursor::open(txn, orderDb);
-                if (cursor.get(indexVal, val, MDB_FIRST)) {
-                        index = lmdb::from_sv<uint64_t>(indexVal);
-                }
-        }
+    auto orderDb     = getEventOrderDb(txn, room_id);
+    auto evToOrderDb = getEventToOrderDb(txn, room_id);
+    auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+    auto order2msgDb = getOrderToMessageDb(txn, room_id);
 
-        uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
-        {
-                auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
-                if (msgCursor.get(indexVal, val, MDB_FIRST)) {
-                        msgIndex = lmdb::from_sv<uint64_t>(indexVal);
-                }
-        }
-
-        if (res.chunk.empty()) {
-                if (orderDb.get(txn, lmdb::to_sv(index), val)) {
-                        auto orderEntry          = json::parse(val);
-                        orderEntry["prev_batch"] = res.end;
-                        orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
-                        txn.commit();
-                }
-                return index;
+    std::string_view indexVal, val;
+    uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
+    {
+        auto cursor = lmdb::cursor::open(txn, orderDb);
+        if (cursor.get(indexVal, val, MDB_FIRST)) {
+            index = lmdb::from_sv<uint64_t>(indexVal);
         }
+    }
 
-        std::string event_id_val;
-        for (const auto &e : res.chunk) {
-                if (std::holds_alternative<
-                      mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e))
-                        continue;
-
-                auto event                = mtx::accessors::serialize_event(e);
-                event_id_val              = event["event_id"].get<std::string>();
-                std::string_view event_id = event_id_val;
-                eventsDb.put(txn, event_id, event.dump());
-
-                --index;
-
-                json orderEntry        = json::object();
-                orderEntry["event_id"] = event_id_val;
-
-                orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
-                evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
-
-                // TODO(Nico): Allow blacklisting more event types in UI
-                if (!isHiddenEvent(txn, e, room_id)) {
-                        --msgIndex;
-                        order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id);
-
-                        msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
-                }
-
-                auto relations = mtx::accessors::relations(e);
-                if (!relations.relations.empty()) {
-                        for (const auto &r : relations.relations) {
-                                if (!r.event_id.empty()) {
-                                        relationsDb.put(txn, r.event_id, event_id);
-                                }
-                        }
+    uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
+    {
+        auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
+        if (msgCursor.get(indexVal, val, MDB_FIRST)) {
+            msgIndex = lmdb::from_sv<uint64_t>(indexVal);
+        }
+    }
+
+    if (res.chunk.empty()) {
+        if (orderDb.get(txn, lmdb::to_sv(index), val)) {
+            auto orderEntry          = json::parse(val);
+            orderEntry["prev_batch"] = res.end;
+            orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
+            txn.commit();
+        }
+        return index;
+    }
+
+    std::string event_id_val;
+    for (const auto &e : res.chunk) {
+        if (std::holds_alternative<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e))
+            continue;
+
+        auto event                = mtx::accessors::serialize_event(e);
+        event_id_val              = event["event_id"].get<std::string>();
+        std::string_view event_id = event_id_val;
+
+        // This check protects against duplicates in the timeline. If the event_id is
+        // already in the DB, we skip putting it (again) in ordered DBs, and only update the
+        // event itself and its relations.
+        std::string_view unused_read;
+        if (!evToOrderDb.get(txn, event_id, unused_read)) {
+            --index;
+
+            json orderEntry        = json::object();
+            orderEntry["event_id"] = event_id_val;
+
+            orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
+            evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
+
+            // TODO(Nico): Allow blacklisting more event types in UI
+            if (!isHiddenEvent(txn, e, room_id)) {
+                --msgIndex;
+                order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id);
+
+                msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
+            }
+        }
+        eventsDb.put(txn, event_id, event.dump());
+
+        auto relations = mtx::accessors::relations(e);
+        if (!relations.relations.empty()) {
+            for (const auto &r : relations.relations) {
+                if (!r.event_id.empty()) {
+                    relationsDb.put(txn, r.event_id, event_id);
                 }
+            }
         }
+    }
 
-        json orderEntry          = json::object();
-        orderEntry["event_id"]   = event_id_val;
-        orderEntry["prev_batch"] = res.end;
-        orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
+    json orderEntry          = json::object();
+    orderEntry["event_id"]   = event_id_val;
+    orderEntry["prev_batch"] = res.end;
+    orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
 
-        txn.commit();
+    txn.commit();
 
-        return msgIndex;
+    return msgIndex;
 }
 
 void
 Cache::clearTimeline(const std::string &room_id)
 {
-        auto txn         = lmdb::txn::begin(env_);
-        auto eventsDb    = getEventsDb(txn, room_id);
-        auto relationsDb = getRelationsDb(txn, room_id);
+    auto txn         = lmdb::txn::begin(env_);
+    auto eventsDb    = getEventsDb(txn, room_id);
+    auto relationsDb = getRelationsDb(txn, room_id);
 
-        auto orderDb     = getEventOrderDb(txn, room_id);
-        auto evToOrderDb = getEventToOrderDb(txn, room_id);
-        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
-        auto order2msgDb = getOrderToMessageDb(txn, room_id);
-
-        std::string_view indexVal, val;
-        auto cursor = lmdb::cursor::open(txn, orderDb);
+    auto orderDb     = getEventOrderDb(txn, room_id);
+    auto evToOrderDb = getEventToOrderDb(txn, room_id);
+    auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+    auto order2msgDb = getOrderToMessageDb(txn, room_id);
 
-        bool start                   = true;
-        bool passed_pagination_token = false;
-        while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
-                start = false;
-                json obj;
+    std::string_view indexVal, val;
+    auto cursor = lmdb::cursor::open(txn, orderDb);
 
-                try {
-                        obj = json::parse(std::string_view(val.data(), val.size()));
-                } catch (std::exception &) {
-                        // workaround bug in the initial db format, where we sometimes didn't store
-                        // json...
-                        obj = {{"event_id", std::string(val.data(), val.size())}};
-                }
+    bool start                   = true;
+    bool passed_pagination_token = false;
+    while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
+        start = false;
+        json obj;
 
-                if (passed_pagination_token) {
-                        if (obj.count("event_id") != 0) {
-                                std::string event_id = obj["event_id"].get<std::string>();
-
-                                if (!event_id.empty()) {
-                                        evToOrderDb.del(txn, event_id);
-                                        eventsDb.del(txn, event_id);
-                                        relationsDb.del(txn, event_id);
-
-                                        std::string_view order{};
-                                        bool exists = msg2orderDb.get(txn, event_id, order);
-                                        if (exists) {
-                                                order2msgDb.del(txn, order);
-                                                msg2orderDb.del(txn, event_id);
-                                        }
-                                }
-                        }
-                        lmdb::cursor_del(cursor);
-                } else {
-                        if (obj.count("prev_batch") != 0)
-                                passed_pagination_token = true;
+        try {
+            obj = json::parse(std::string_view(val.data(), val.size()));
+        } catch (std::exception &) {
+            // workaround bug in the initial db format, where we sometimes didn't store
+            // json...
+            obj = {{"event_id", std::string(val.data(), val.size())}};
+        }
+
+        if (passed_pagination_token) {
+            if (obj.count("event_id") != 0) {
+                std::string event_id = obj["event_id"].get<std::string>();
+
+                if (!event_id.empty()) {
+                    evToOrderDb.del(txn, event_id);
+                    eventsDb.del(txn, event_id);
+                    relationsDb.del(txn, event_id);
+
+                    std::string_view order{};
+                    bool exists = msg2orderDb.get(txn, event_id, order);
+                    if (exists) {
+                        order2msgDb.del(txn, order);
+                        msg2orderDb.del(txn, event_id);
+                    }
                 }
+            }
+            lmdb::cursor_del(cursor);
+        } else {
+            if (obj.count("prev_batch") != 0)
+                passed_pagination_token = true;
         }
+    }
 
-        auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
-        start          = true;
-        while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
-                start = false;
-
-                std::string_view eventId;
-                bool innerStart = true;
-                bool found      = false;
-                while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) {
-                        innerStart = false;
-
-                        json obj;
-                        try {
-                                obj = json::parse(std::string_view(eventId.data(), eventId.size()));
-                        } catch (std::exception &) {
-                                obj = {{"event_id", std::string(eventId.data(), eventId.size())}};
-                        }
+    auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
+    start          = true;
+    while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
+        start = false;
 
-                        if (obj["event_id"] == std::string(val.data(), val.size())) {
-                                found = true;
-                                break;
-                        }
-                }
+        std::string_view eventId;
+        bool innerStart = true;
+        bool found      = false;
+        while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) {
+            innerStart = false;
 
-                if (!found)
-                        break;
+            json obj;
+            try {
+                obj = json::parse(std::string_view(eventId.data(), eventId.size()));
+            } catch (std::exception &) {
+                obj = {{"event_id", std::string(eventId.data(), eventId.size())}};
+            }
+
+            if (obj["event_id"] == std::string(val.data(), val.size())) {
+                found = true;
+                break;
+            }
         }
 
-        do {
-                lmdb::cursor_del(msgCursor);
-        } while (msgCursor.get(indexVal, val, MDB_PREV));
+        if (!found)
+            break;
+    }
 
-        cursor.close();
-        msgCursor.close();
-        txn.commit();
+    do {
+        lmdb::cursor_del(msgCursor);
+    } while (msgCursor.get(indexVal, val, MDB_PREV));
+
+    cursor.close();
+    msgCursor.close();
+    txn.commit();
 }
 
 mtx::responses::Notifications
 Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id)
 {
-        auto db = getMentionsDb(txn, room_id);
+    auto db = getMentionsDb(txn, room_id);
 
-        if (db.size(txn) == 0) {
-                return mtx::responses::Notifications{};
-        }
+    if (db.size(txn) == 0) {
+        return mtx::responses::Notifications{};
+    }
 
-        mtx::responses::Notifications notif;
-        std::string_view event_id, msg;
+    mtx::responses::Notifications notif;
+    std::string_view event_id, msg;
 
-        auto cursor = lmdb::cursor::open(txn, db);
+    auto cursor = lmdb::cursor::open(txn, db);
 
-        while (cursor.get(event_id, msg, MDB_NEXT)) {
-                auto obj = json::parse(msg);
+    while (cursor.get(event_id, msg, MDB_NEXT)) {
+        auto obj = json::parse(msg);
 
-                if (obj.count("event") == 0)
-                        continue;
+        if (obj.count("event") == 0)
+            continue;
 
-                mtx::responses::Notification notification;
-                mtx::responses::from_json(obj, notification);
+        mtx::responses::Notification notification;
+        mtx::responses::from_json(obj, notification);
 
-                notif.notifications.push_back(notification);
-        }
-        cursor.close();
+        notif.notifications.push_back(notification);
+    }
+    cursor.close();
 
-        std::reverse(notif.notifications.begin(), notif.notifications.end());
+    std::reverse(notif.notifications.begin(), notif.notifications.end());
 
-        return notif;
+    return notif;
 }
 
 //! Add all notifications containing a user mention to the db.
 void
 Cache::saveTimelineMentions(const mtx::responses::Notifications &res)
 {
-        QMap<std::string, QList<mtx::responses::Notification>> notifsByRoom;
+    QMap<std::string, QList<mtx::responses::Notification>> notifsByRoom;
 
-        // Sort into room-specific 'buckets'
-        for (const auto &notif : res.notifications) {
-                json val = notif;
-                notifsByRoom[notif.room_id].push_back(notif);
-        }
+    // Sort into room-specific 'buckets'
+    for (const auto &notif : res.notifications) {
+        json val = notif;
+        notifsByRoom[notif.room_id].push_back(notif);
+    }
 
-        auto txn = lmdb::txn::begin(env_);
-        // Insert the entire set of mentions for each room at a time.
-        QMap<std::string, QList<mtx::responses::Notification>>::const_iterator it =
-          notifsByRoom.constBegin();
-        auto end = notifsByRoom.constEnd();
-        while (it != end) {
-                nhlog::db()->debug("Storing notifications for " + it.key());
-                saveTimelineMentions(txn, it.key(), std::move(it.value()));
-                ++it;
-        }
+    auto txn = lmdb::txn::begin(env_);
+    // Insert the entire set of mentions for each room at a time.
+    QMap<std::string, QList<mtx::responses::Notification>>::const_iterator it =
+      notifsByRoom.constBegin();
+    auto end = notifsByRoom.constEnd();
+    while (it != end) {
+        nhlog::db()->debug("Storing notifications for " + it.key());
+        saveTimelineMentions(txn, it.key(), std::move(it.value()));
+        ++it;
+    }
 
-        txn.commit();
+    txn.commit();
 }
 
 void
@@ -3148,139 +3254,138 @@ Cache::saveTimelineMentions(lmdb::txn &txn,
                             const std::string &room_id,
                             const QList<mtx::responses::Notification> &res)
 {
-        auto db = getMentionsDb(txn, room_id);
+    auto db = getMentionsDb(txn, room_id);
 
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        for (const auto &notif : res) {
-                const auto event_id = mtx::accessors::event_id(notif.event);
+    for (const auto &notif : res) {
+        const auto event_id = mtx::accessors::event_id(notif.event);
 
-                // double check that we have the correct room_id...
-                if (room_id.compare(notif.room_id) != 0) {
-                        return;
-                }
+        // double check that we have the correct room_id...
+        if (room_id.compare(notif.room_id) != 0) {
+            return;
+        }
 
-                json obj = notif;
+        json obj = notif;
 
-                db.put(txn, event_id, obj.dump());
-        }
+        db.put(txn, event_id, obj.dump());
+    }
 }
 
 void
 Cache::markSentNotification(const std::string &event_id)
-{
-        auto txn = lmdb::txn::begin(env_);
-        notificationsDb_.put(txn, event_id, "");
-        txn.commit();
+{
+    auto txn = lmdb::txn::begin(env_);
+    notificationsDb_.put(txn, event_id, "");
+    txn.commit();
 }
 
 void
 Cache::removeReadNotification(const std::string &event_id)
 {
-        auto txn = lmdb::txn::begin(env_);
+    auto txn = lmdb::txn::begin(env_);
 
-        notificationsDb_.del(txn, event_id);
+    notificationsDb_.del(txn, event_id);
 
-        txn.commit();
+    txn.commit();
 }
 
 bool
 Cache::isNotificationSent(const std::string &event_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::string_view value;
-        bool res = notificationsDb_.get(txn, event_id, value);
+    std::string_view value;
+    bool res = notificationsDb_.get(txn, event_id, value);
 
-        return res;
+    return res;
 }
 
 std::vector<std::string>
 Cache::getRoomIds(lmdb::txn &txn)
 {
-        auto db     = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
-        auto cursor = lmdb::cursor::open(txn, db);
+    auto cursor = lmdb::cursor::open(txn, roomsDb_);
 
-        std::vector<std::string> rooms;
+    std::vector<std::string> rooms;
 
-        std::string_view room_id, _unused;
-        while (cursor.get(room_id, _unused, MDB_NEXT))
-                rooms.emplace_back(room_id);
+    std::string_view room_id, _unused;
+    while (cursor.get(room_id, _unused, MDB_NEXT))
+        rooms.emplace_back(room_id);
 
-        cursor.close();
+    cursor.close();
 
-        return rooms;
+    return rooms;
 }
 
 void
 Cache::deleteOldMessages()
 {
-        std::string_view indexVal, val;
+    std::string_view indexVal, val;
 
-        auto txn      = lmdb::txn::begin(env_);
-        auto room_ids = getRoomIds(txn);
+    auto txn      = lmdb::txn::begin(env_);
+    auto room_ids = getRoomIds(txn);
 
-        for (const auto &room_id : room_ids) {
-                auto orderDb     = getEventOrderDb(txn, room_id);
-                auto evToOrderDb = getEventToOrderDb(txn, room_id);
-                auto o2m         = getOrderToMessageDb(txn, room_id);
-                auto m2o         = getMessageToOrderDb(txn, room_id);
-                auto eventsDb    = getEventsDb(txn, room_id);
-                auto relationsDb = getRelationsDb(txn, room_id);
-                auto cursor      = lmdb::cursor::open(txn, orderDb);
+    for (const auto &room_id : room_ids) {
+        auto orderDb     = getEventOrderDb(txn, room_id);
+        auto evToOrderDb = getEventToOrderDb(txn, room_id);
+        auto o2m         = getOrderToMessageDb(txn, room_id);
+        auto m2o         = getMessageToOrderDb(txn, room_id);
+        auto eventsDb    = getEventsDb(txn, room_id);
+        auto relationsDb = getRelationsDb(txn, room_id);
+        auto cursor      = lmdb::cursor::open(txn, orderDb);
 
-                uint64_t first, last;
-                if (cursor.get(indexVal, val, MDB_LAST)) {
-                        last = lmdb::from_sv<uint64_t>(indexVal);
-                } else {
-                        continue;
-                }
-                if (cursor.get(indexVal, val, MDB_FIRST)) {
-                        first = lmdb::from_sv<uint64_t>(indexVal);
-                } else {
-                        continue;
-                }
+        uint64_t first, last;
+        if (cursor.get(indexVal, val, MDB_LAST)) {
+            last = lmdb::from_sv<uint64_t>(indexVal);
+        } else {
+            continue;
+        }
+        if (cursor.get(indexVal, val, MDB_FIRST)) {
+            first = lmdb::from_sv<uint64_t>(indexVal);
+        } else {
+            continue;
+        }
 
-                size_t message_count = static_cast<size_t>(last - first);
-                if (message_count < MAX_RESTORED_MESSAGES)
-                        continue;
-
-                bool start = true;
-                while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) &&
-                       message_count-- > MAX_RESTORED_MESSAGES) {
-                        start    = false;
-                        auto obj = json::parse(std::string_view(val.data(), val.size()));
-
-                        if (obj.count("event_id") != 0) {
-                                std::string event_id = obj["event_id"].get<std::string>();
-                                evToOrderDb.del(txn, event_id);
-                                eventsDb.del(txn, event_id);
-
-                                relationsDb.del(txn, event_id);
-
-                                std::string_view order{};
-                                bool exists = m2o.get(txn, event_id, order);
-                                if (exists) {
-                                        o2m.del(txn, order);
-                                        m2o.del(txn, event_id);
-                                }
-                        }
-                        cursor.del();
+        size_t message_count = static_cast<size_t>(last - first);
+        if (message_count < MAX_RESTORED_MESSAGES)
+            continue;
+
+        bool start = true;
+        while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) &&
+               message_count-- > MAX_RESTORED_MESSAGES) {
+            start    = false;
+            auto obj = json::parse(std::string_view(val.data(), val.size()));
+
+            if (obj.count("event_id") != 0) {
+                std::string event_id = obj["event_id"].get<std::string>();
+                evToOrderDb.del(txn, event_id);
+                eventsDb.del(txn, event_id);
+
+                relationsDb.del(txn, event_id);
+
+                std::string_view order{};
+                bool exists = m2o.get(txn, event_id, order);
+                if (exists) {
+                    o2m.del(txn, order);
+                    m2o.del(txn, event_id);
                 }
-                cursor.close();
+            }
+            cursor.del();
         }
-        txn.commit();
+        cursor.close();
+    }
+    txn.commit();
 }
 
 void
 Cache::deleteOldData() noexcept
 {
-        try {
-                deleteOldMessages();
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("failed to delete old messages: {}", e.what());
-        }
+    try {
+        deleteOldMessages();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("failed to delete old messages: {}", e.what());
+    }
 }
 
 void
@@ -3288,241 +3393,245 @@ Cache::updateSpaces(lmdb::txn &txn,
                     const std::set<std::string> &spaces_with_updates,
                     std::set<std::string> rooms_with_updates)
 {
-        if (spaces_with_updates.empty() && rooms_with_updates.empty())
-                return;
+    if (spaces_with_updates.empty() && rooms_with_updates.empty())
+        return;
 
-        for (const auto &space : spaces_with_updates) {
-                // delete old entries
-                {
-                        auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
-                        bool first          = true;
-                        std::string_view sp = space, space_child = "";
-
-                        if (cursor.get(sp, space_child, MDB_SET)) {
-                                while (cursor.get(
-                                  sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                        first = false;
-                                        spacesParentsDb_.del(txn, space_child, space);
-                                }
-                        }
-                        cursor.close();
-                        spacesChildrenDb_.del(txn, space);
+    for (const auto &space : spaces_with_updates) {
+        // delete old entries
+        {
+            auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
+            bool first          = true;
+            std::string_view sp = space, space_child = "";
+
+            if (cursor.get(sp, space_child, MDB_SET)) {
+                while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                    first = false;
+                    spacesParentsDb_.del(txn, space_child, space);
                 }
+            }
+            cursor.close();
+            spacesChildrenDb_.del(txn, space);
+        }
 
-                for (const auto &event :
-                     getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) {
-                        if (event.content.via.has_value() && event.state_key.size() > 3 &&
-                            event.state_key.at(0) == '!') {
-                                spacesChildrenDb_.put(txn, space, event.state_key);
-                                spacesParentsDb_.put(txn, event.state_key, space);
-                        }
-                }
+        for (const auto &event :
+             getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) {
+            if (event.content.via.has_value() && event.state_key.size() > 3 &&
+                event.state_key.at(0) == '!') {
+                spacesChildrenDb_.put(txn, space, event.state_key);
+                spacesParentsDb_.put(txn, event.state_key, space);
+            }
         }
 
-        const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels);
+        for (const auto &r : getRoomIds(txn)) {
+            if (auto parent = getStateEvent<mtx::events::state::space::Parent>(txn, r, space)) {
+                rooms_with_updates.insert(r);
+            }
+        }
+    }
 
-        for (const auto &room : rooms_with_updates) {
-                for (const auto &event :
-                     getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) {
-                        if (event.content.via.has_value() && event.state_key.size() > 3 &&
-                            event.state_key.at(0) == '!') {
-                                const std::string &space = event.state_key;
+    const auto space_event_type = to_string(mtx::events::EventType::SpaceChild);
 
-                                auto pls =
-                                  getStateEvent<mtx::events::state::PowerLevels>(txn, space);
+    for (const auto &room : rooms_with_updates) {
+        for (const auto &event :
+             getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) {
+            if (event.content.via.has_value() && event.state_key.size() > 3 &&
+                event.state_key.at(0) == '!') {
+                const std::string &space = event.state_key;
 
-                                if (!pls)
-                                        continue;
+                auto pls = getStateEvent<mtx::events::state::PowerLevels>(txn, space);
 
-                                if (pls->content.user_level(event.sender) >=
-                                    pls->content.state_level(space_event_type)) {
-                                        spacesChildrenDb_.put(txn, space, room);
-                                        spacesParentsDb_.put(txn, room, space);
-                                }
-                        }
+                if (!pls)
+                    continue;
+
+                if (pls->content.user_level(event.sender) >=
+                    pls->content.state_level(space_event_type)) {
+                    spacesChildrenDb_.put(txn, space, room);
+                    spacesParentsDb_.put(txn, room, space);
+                } else {
+                    nhlog::db()->debug("Skipping {} in {} because of missing PL. {}: {} < {}",
+                                       room,
+                                       space,
+                                       event.sender,
+                                       pls->content.user_level(event.sender),
+                                       pls->content.state_level(space_event_type));
                 }
+            }
         }
+    }
 }
 
 QMap<QString, std::optional<RoomInfo>>
 Cache::spaces()
 {
-        auto txn = ro_txn(env_);
-
-        QMap<QString, std::optional<RoomInfo>> ret;
-        {
-                auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
-                bool first  = true;
-                std::string_view space_id, space_child;
-                while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) {
-                        first = false;
-
-                        if (!space_child.empty()) {
-                                std::string_view room_data;
-                                if (roomsDb_.get(txn, space_id, room_data)) {
-                                        RoomInfo tmp = json::parse(std::move(room_data));
-                                        ret.insert(
-                                          QString::fromUtf8(space_id.data(), space_id.size()), tmp);
-                                } else {
-                                        ret.insert(
-                                          QString::fromUtf8(space_id.data(), space_id.size()),
-                                          std::nullopt);
-                                }
-                        }
+    auto txn = ro_txn(env_);
+
+    QMap<QString, std::optional<RoomInfo>> ret;
+    {
+        auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
+        bool first  = true;
+        std::string_view space_id, space_child;
+        while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) {
+            first = false;
+
+            if (!space_child.empty()) {
+                std::string_view room_data;
+                if (roomsDb_.get(txn, space_id, room_data)) {
+                    RoomInfo tmp = json::parse(std::move(room_data));
+                    ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), tmp);
+                } else {
+                    ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), std::nullopt);
                 }
-                cursor.close();
+            }
         }
+        cursor.close();
+    }
 
-        return ret;
+    return ret;
 }
 
 std::vector<std::string>
 Cache::getParentRoomIds(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector<std::string> roomids;
-        {
-                auto cursor         = lmdb::cursor::open(txn, spacesParentsDb_);
-                bool first          = true;
-                std::string_view sp = room_id, space_parent;
-                if (cursor.get(sp, space_parent, MDB_SET)) {
-                        while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                first = false;
-
-                                if (!space_parent.empty())
-                                        roomids.emplace_back(space_parent);
-                        }
-                }
-                cursor.close();
+    std::vector<std::string> roomids;
+    {
+        auto cursor         = lmdb::cursor::open(txn, spacesParentsDb_);
+        bool first          = true;
+        std::string_view sp = room_id, space_parent;
+        if (cursor.get(sp, space_parent, MDB_SET)) {
+            while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                first = false;
+
+                if (!space_parent.empty())
+                    roomids.emplace_back(space_parent);
+            }
         }
+        cursor.close();
+    }
 
-        return roomids;
+    return roomids;
 }
 
 std::vector<std::string>
 Cache::getChildRoomIds(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector<std::string> roomids;
-        {
-                auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
-                bool first          = true;
-                std::string_view sp = room_id, space_child;
-                if (cursor.get(sp, space_child, MDB_SET)) {
-                        while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                first = false;
-
-                                if (!space_child.empty())
-                                        roomids.emplace_back(space_child);
-                        }
-                }
-                cursor.close();
+    std::vector<std::string> roomids;
+    {
+        auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
+        bool first          = true;
+        std::string_view sp = room_id, space_child;
+        if (cursor.get(sp, space_child, MDB_SET)) {
+            while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                first = false;
+
+                if (!space_child.empty())
+                    roomids.emplace_back(space_child);
+            }
         }
+        cursor.close();
+    }
 
-        return roomids;
+    return roomids;
 }
 
 std::vector<ImagePackInfo>
 Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
 {
-        auto txn = ro_txn(env_);
-        std::vector<ImagePackInfo> infos;
-
-        auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack,
-                                          const std::string &source_room,
-                                          const std::string &state_key) {
-                if (!pack.pack || !stickers.has_value() ||
-                    (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
-                        ImagePackInfo info;
-                        info.source_room = source_room;
-                        info.state_key   = state_key;
-                        info.pack.pack   = pack.pack;
-
-                        for (const auto &img : pack.images) {
-                                if (stickers.has_value() && img.second.overrides_usage() &&
-                                    (stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
-                                        continue;
-
-                                info.pack.images.insert(img);
-                        }
-
-                        if (!info.pack.images.empty())
-                                infos.push_back(std::move(info));
-                }
-        };
-
-        // packs from account data
-        if (auto accountpack =
-              getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) {
-                auto tmp =
-                  std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(
-                    &*accountpack);
-                if (tmp)
-                        addPack(tmp->content, "", "");
-        }
-
-        // packs from rooms, that were enabled globally
-        if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) {
-                auto tmp =
-                  std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
-                    &*roomPacks);
-                if (tmp) {
-                        for (const auto &[room_id2, state_to_d] : tmp->content.rooms) {
-                                // don't add stickers from this room twice
-                                if (room_id2 == room_id)
-                                        continue;
-
-                                for (const auto &[state_id, d] : state_to_d) {
-                                        (void)d;
-                                        if (auto pack =
-                                              getStateEvent<mtx::events::msc2545::ImagePack>(
-                                                txn, room_id2, state_id))
-                                                addPack(pack->content, room_id2, state_id);
-                                }
-                        }
+    auto txn = ro_txn(env_);
+    std::vector<ImagePackInfo> infos;
+
+    auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack,
+                                      const std::string &source_room,
+                                      const std::string &state_key) {
+        if (!pack.pack || !stickers.has_value() ||
+            (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
+            ImagePackInfo info;
+            info.source_room = source_room;
+            info.state_key   = state_key;
+            info.pack.pack   = pack.pack;
+
+            for (const auto &img : pack.images) {
+                if (stickers.has_value() && img.second.overrides_usage() &&
+                    (stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
+                    continue;
+
+                info.pack.images.insert(img);
+            }
+
+            if (!info.pack.images.empty())
+                infos.push_back(std::move(info));
+        }
+    };
+
+    // packs from account data
+    if (auto accountpack =
+          getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) {
+        auto tmp =
+          std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(&*accountpack);
+        if (tmp)
+            addPack(tmp->content, "", "");
+    }
+
+    // packs from rooms, that were enabled globally
+    if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) {
+        auto tmp = std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
+          &*roomPacks);
+        if (tmp) {
+            for (const auto &[room_id2, state_to_d] : tmp->content.rooms) {
+                // don't add stickers from this room twice
+                if (room_id2 == room_id)
+                    continue;
+
+                for (const auto &[state_id, d] : state_to_d) {
+                    (void)d;
+                    if (auto pack =
+                          getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id2, state_id))
+                        addPack(pack->content, room_id2, state_id);
                 }
+            }
         }
+    }
 
-        // packs from current room
-        if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) {
-                addPack(pack->content, room_id, "");
-        }
-        for (const auto &pack :
-             getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) {
-                addPack(pack.content, room_id, pack.state_key);
-        }
+    // packs from current room
+    if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) {
+        addPack(pack->content, room_id, "");
+    }
+    for (const auto &pack : getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) {
+        addPack(pack.content, room_id, pack.state_key);
+    }
 
-        return infos;
+    return infos;
 }
 
 std::optional<mtx::events::collections::RoomAccountDataEvents>
 Cache::getAccountData(mtx::events::EventType type, const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
-        return getAccountData(txn, type, room_id);
+    auto txn = ro_txn(env_);
+    return getAccountData(txn, type, room_id);
 }
 
 std::optional<mtx::events::collections::RoomAccountDataEvents>
 Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
 {
-        try {
-                auto db = getAccountDataDb(txn, room_id);
-
-                std::string_view data;
-                if (db.get(txn, to_string(type), data)) {
-                        mtx::responses::utils::RoomAccountDataEvents events;
-                        json j = json::array({
-                          json::parse(data),
-                        });
-                        mtx::responses::utils::parse_room_account_data_events(j, events);
-                        if (events.size() == 1)
-                                return events.front();
-                }
-        } catch (...) {
+    try {
+        auto db = getAccountDataDb(txn, room_id);
+
+        std::string_view data;
+        if (db.get(txn, to_string(type), data)) {
+            mtx::responses::utils::RoomAccountDataEvents events;
+            json j = json::array({
+              json::parse(data),
+            });
+            mtx::responses::utils::parse_room_account_data_events(j, events);
+            if (events.size() == 1)
+                return events.front();
         }
-        return std::nullopt;
+    } catch (...) {
+    }
+    return std::nullopt;
 }
 
 bool
@@ -3530,470 +3639,446 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
                            const std::string &room_id,
                            const std::string &user_id)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getStatesDb(txn, room_id);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getStatesDb(txn, room_id);
 
-        int64_t min_event_level = std::numeric_limits<int64_t>::max();
-        int64_t user_level      = std::numeric_limits<int64_t>::min();
+    int64_t min_event_level = std::numeric_limits<int64_t>::max();
+    int64_t user_level      = std::numeric_limits<int64_t>::min();
 
-        std::string_view event;
-        bool res = db.get(txn, to_string(EventType::RoomPowerLevels), event);
+    std::string_view event;
+    bool res = db.get(txn, to_string(EventType::RoomPowerLevels), event);
 
-        if (res) {
-                try {
-                        StateEvent<PowerLevels> msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    if (res) {
+        try {
+            StateEvent<PowerLevels> msg = json::parse(std::string_view(event.data(), event.size()));
 
-                        user_level = msg.content.user_level(user_id);
+            user_level = msg.content.user_level(user_id);
 
-                        for (const auto &ty : eventTypes)
-                                min_event_level =
-                                  std::min(min_event_level, msg.content.state_level(to_string(ty)));
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.power_levels event: {}",
-                                          e.what());
-                }
+            for (const auto &ty : eventTypes)
+                min_event_level = std::min(min_event_level, msg.content.state_level(to_string(ty)));
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what());
         }
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return user_level >= min_event_level;
+    return user_level >= min_event_level;
 }
 
 std::vector<std::string>
 Cache::roomMembers(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector<std::string> members;
-        std::string_view user_id, unused;
+    std::vector<std::string> members;
+    std::string_view user_id, unused;
 
-        auto db = getMembersDb(txn, room_id);
+    auto db = getMembersDb(txn, room_id);
 
-        auto cursor = lmdb::cursor::open(txn, db);
-        while (cursor.get(user_id, unused, MDB_NEXT))
-                members.emplace_back(user_id);
-        cursor.close();
+    auto cursor = lmdb::cursor::open(txn, db);
+    while (cursor.get(user_id, unused, MDB_NEXT))
+        members.emplace_back(user_id);
+    cursor.close();
 
-        return members;
+    return members;
 }
 
 crypto::Trust
 Cache::roomVerificationStatus(const std::string &room_id)
 {
-        crypto::Trust trust = crypto::Verified;
+    crypto::Trust trust = crypto::Verified;
 
-        try {
-                auto txn = lmdb::txn::begin(env_);
-
-                auto db     = getMembersDb(txn, room_id);
-                auto keysDb = getUserKeysDb(txn);
-                std::vector<std::string> keysToRequest;
-
-                std::string_view user_id, unused;
-                auto cursor = lmdb::cursor::open(txn, db);
-                while (cursor.get(user_id, unused, MDB_NEXT)) {
-                        auto verif = verificationStatus_(std::string(user_id), txn);
-                        if (verif.unverified_device_count) {
-                                trust = crypto::Unverified;
-                                if (verif.verified_devices.empty() && verif.no_keys) {
-                                        // we probably don't have the keys yet, so query them
-                                        keysToRequest.push_back(std::string(user_id));
-                                }
-                        } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified)
-                                trust = crypto::TOFU;
-                }
+    try {
+        auto txn = lmdb::txn::begin(env_);
 
-                if (!keysToRequest.empty())
-                        markUserKeysOutOfDate(txn, keysDb, keysToRequest, "");
+        auto db     = getMembersDb(txn, room_id);
+        auto keysDb = getUserKeysDb(txn);
+        std::vector<std::string> keysToRequest;
 
-        } catch (std::exception &e) {
-                nhlog::db()->error(
-                  "Failed to calculate verification status for {}: {}", room_id, e.what());
+        std::string_view user_id, unused;
+        auto cursor = lmdb::cursor::open(txn, db);
+        while (cursor.get(user_id, unused, MDB_NEXT)) {
+            auto verif = verificationStatus_(std::string(user_id), txn);
+            if (verif.unverified_device_count) {
                 trust = crypto::Unverified;
+                if (verif.verified_devices.empty() && verif.no_keys) {
+                    // we probably don't have the keys yet, so query them
+                    keysToRequest.push_back(std::string(user_id));
+                }
+            } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified)
+                trust = crypto::TOFU;
         }
 
-        return trust;
+        if (!keysToRequest.empty())
+            markUserKeysOutOfDate(txn, keysDb, keysToRequest, "");
+
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to calculate verification status for {}: {}", room_id, e.what());
+        trust = crypto::Unverified;
+    }
+
+    return trust;
 }
 
 std::map<std::string, std::optional<UserKeyCache>>
 Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
 {
-        std::string_view keys;
+    std::string_view keys;
 
-        try {
-                auto txn = ro_txn(env_);
-                std::map<std::string, std::optional<UserKeyCache>> members;
-
-                auto db     = getMembersDb(txn, room_id);
-                auto keysDb = getUserKeysDb(txn);
-
-                std::string_view user_id, unused;
-                auto cursor = lmdb::cursor::open(txn, db);
-                while (cursor.get(user_id, unused, MDB_NEXT)) {
-                        auto res = keysDb.get(txn, user_id, keys);
-
-                        if (res) {
-                                auto k = json::parse(keys).get<UserKeyCache>();
-                                if (verified_only) {
-                                        auto verif = verificationStatus(std::string(user_id));
-                                        if (verif.user_verified == crypto::Trust::Verified ||
-                                            !verif.verified_devices.empty()) {
-                                                auto keyCopy = k;
-                                                keyCopy.device_keys.clear();
-
-                                                std::copy_if(
-                                                  k.device_keys.begin(),
-                                                  k.device_keys.end(),
-                                                  std::inserter(keyCopy.device_keys,
-                                                                keyCopy.device_keys.end()),
-                                                  [&verif](const auto &key) {
-                                                          auto curve25519 = key.second.keys.find(
-                                                            "curve25519:" + key.first);
-                                                          if (curve25519 == key.second.keys.end())
-                                                                  return false;
-                                                          if (auto t =
-                                                                verif.verified_device_keys.find(
-                                                                  curve25519->second);
-                                                              t ==
-                                                                verif.verified_device_keys.end() ||
-                                                              t->second != crypto::Trust::Verified)
-                                                                  return false;
-
-                                                          return key.first ==
-                                                                   key.second.device_id &&
-                                                                 std::find(
-                                                                   verif.verified_devices.begin(),
-                                                                   verif.verified_devices.end(),
-                                                                   key.first) !=
-                                                                   verif.verified_devices.end();
-                                                  });
-
-                                                if (!keyCopy.device_keys.empty())
-                                                        members[std::string(user_id)] =
-                                                          std::move(keyCopy);
-                                        }
-                                } else {
-                                        members[std::string(user_id)] = std::move(k);
-                                }
-                        } else {
-                                if (!verified_only)
-                                        members[std::string(user_id)] = {};
-                        }
-                }
-                cursor.close();
+    try {
+        auto txn = ro_txn(env_);
+        std::map<std::string, std::optional<UserKeyCache>> members;
 
-                return members;
-        } catch (std::exception &) {
-                return {};
+        auto db     = getMembersDb(txn, room_id);
+        auto keysDb = getUserKeysDb(txn);
+
+        std::string_view user_id, unused;
+        auto cursor = lmdb::cursor::open(txn, db);
+        while (cursor.get(user_id, unused, MDB_NEXT)) {
+            auto res = keysDb.get(txn, user_id, keys);
+
+            if (res) {
+                auto k = json::parse(keys).get<UserKeyCache>();
+                if (verified_only) {
+                    auto verif = verificationStatus_(std::string(user_id), txn);
+
+                    if (verif.user_verified == crypto::Trust::Verified ||
+                        !verif.verified_devices.empty()) {
+                        auto keyCopy = k;
+                        keyCopy.device_keys.clear();
+
+                        std::copy_if(
+                          k.device_keys.begin(),
+                          k.device_keys.end(),
+                          std::inserter(keyCopy.device_keys, keyCopy.device_keys.end()),
+                          [&verif](const auto &key) {
+                              auto curve25519 = key.second.keys.find("curve25519:" + key.first);
+                              if (curve25519 == key.second.keys.end())
+                                  return false;
+                              if (auto t = verif.verified_device_keys.find(curve25519->second);
+                                  t == verif.verified_device_keys.end() ||
+                                  t->second != crypto::Trust::Verified)
+                                  return false;
+
+                              return key.first == key.second.device_id &&
+                                     std::find(verif.verified_devices.begin(),
+                                               verif.verified_devices.end(),
+                                               key.first) != verif.verified_devices.end();
+                          });
+
+                        if (!keyCopy.device_keys.empty())
+                            members[std::string(user_id)] = std::move(keyCopy);
+                    }
+                } else {
+                    members[std::string(user_id)] = std::move(k);
+                }
+            } else {
+                if (!verified_only)
+                    members[std::string(user_id)] = {};
+            }
         }
+        cursor.close();
+
+        return members;
+    } catch (std::exception &e) {
+        nhlog::db()->debug("Error retrieving members: {}", e.what());
+        return {};
+    }
 }
 
 QString
 Cache::displayName(const QString &room_id, const QString &user_id)
 {
-        if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
-            info && !info->name.empty())
-                return QString::fromStdString(info->name);
+    if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
+        info && !info->name.empty())
+        return QString::fromStdString(info->name);
 
-        return user_id;
+    return user_id;
 }
 
 std::string
 Cache::displayName(const std::string &room_id, const std::string &user_id)
 {
-        if (auto info = getMember(room_id, user_id); info && !info->name.empty())
-                return info->name;
+    if (auto info = getMember(room_id, user_id); info && !info->name.empty())
+        return info->name;
 
-        return user_id;
+    return user_id;
 }
 
 QString
 Cache::avatarUrl(const QString &room_id, const QString &user_id)
 {
-        if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
-            info && !info->avatar_url.empty())
-                return QString::fromStdString(info->avatar_url);
+    if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
+        info && !info->avatar_url.empty())
+        return QString::fromStdString(info->avatar_url);
 
-        return "";
+    return "";
 }
 
 mtx::presence::PresenceState
 Cache::presenceState(const std::string &user_id)
 {
-        if (user_id.empty())
-                return {};
+    if (user_id.empty())
+        return {};
 
-        std::string_view presenceVal;
+    std::string_view presenceVal;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getPresenceDb(txn);
-        auto res = db.get(txn, user_id, presenceVal);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getPresenceDb(txn);
+    auto res = db.get(txn, user_id, presenceVal);
 
-        mtx::presence::PresenceState state = mtx::presence::offline;
+    mtx::presence::PresenceState state = mtx::presence::offline;
 
-        if (res) {
-                mtx::events::presence::Presence presence =
-                  json::parse(std::string_view(presenceVal.data(), presenceVal.size()));
-                state = presence.presence;
-        }
+    if (res) {
+        mtx::events::presence::Presence presence =
+          json::parse(std::string_view(presenceVal.data(), presenceVal.size()));
+        state = presence.presence;
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return state;
+    return state;
 }
 
 std::string
 Cache::statusMessage(const std::string &user_id)
 {
-        if (user_id.empty())
-                return {};
+    if (user_id.empty())
+        return {};
 
-        std::string_view presenceVal;
+    std::string_view presenceVal;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getPresenceDb(txn);
-        auto res = db.get(txn, user_id, presenceVal);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getPresenceDb(txn);
+    auto res = db.get(txn, user_id, presenceVal);
 
-        std::string status_msg;
+    std::string status_msg;
 
-        if (res) {
-                mtx::events::presence::Presence presence = json::parse(presenceVal);
-                status_msg                               = presence.status_msg;
-        }
+    if (res) {
+        mtx::events::presence::Presence presence = json::parse(presenceVal);
+        status_msg                               = presence.status_msg;
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return status_msg;
+    return status_msg;
 }
 
 void
 to_json(json &j, const UserKeyCache &info)
 {
-        j["device_keys"]        = info.device_keys;
-        j["seen_device_keys"]   = info.seen_device_keys;
-        j["seen_device_ids"]    = info.seen_device_ids;
-        j["master_keys"]        = info.master_keys;
-        j["master_key_changed"] = info.master_key_changed;
-        j["user_signing_keys"]  = info.user_signing_keys;
-        j["self_signing_keys"]  = info.self_signing_keys;
-        j["updated_at"]         = info.updated_at;
-        j["last_changed"]       = info.last_changed;
+    j["device_keys"]        = info.device_keys;
+    j["seen_device_keys"]   = info.seen_device_keys;
+    j["seen_device_ids"]    = info.seen_device_ids;
+    j["master_keys"]        = info.master_keys;
+    j["master_key_changed"] = info.master_key_changed;
+    j["user_signing_keys"]  = info.user_signing_keys;
+    j["self_signing_keys"]  = info.self_signing_keys;
+    j["updated_at"]         = info.updated_at;
+    j["last_changed"]       = info.last_changed;
 }
 
 void
 from_json(const json &j, UserKeyCache &info)
 {
-        info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{});
-        info.seen_device_keys   = j.value("seen_device_keys", std::set<std::string>{});
-        info.seen_device_ids    = j.value("seen_device_ids", std::set<std::string>{});
-        info.master_keys        = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
-        info.master_key_changed = j.value("master_key_changed", false);
-        info.user_signing_keys  = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
-        info.self_signing_keys  = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{});
-        info.updated_at         = j.value("updated_at", "");
-        info.last_changed       = j.value("last_changed", "");
+    info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{});
+    info.seen_device_keys   = j.value("seen_device_keys", std::set<std::string>{});
+    info.seen_device_ids    = j.value("seen_device_ids", std::set<std::string>{});
+    info.master_keys        = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
+    info.master_key_changed = j.value("master_key_changed", false);
+    info.user_signing_keys  = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
+    info.self_signing_keys  = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{});
+    info.updated_at         = j.value("updated_at", "");
+    info.last_changed       = j.value("last_changed", "");
 }
 
 std::optional<UserKeyCache>
 Cache::userKeys(const std::string &user_id)
 {
-        auto txn = ro_txn(env_);
-        return userKeys_(user_id, txn);
+    auto txn = ro_txn(env_);
+    return userKeys_(user_id, txn);
 }
 
 std::optional<UserKeyCache>
 Cache::userKeys_(const std::string &user_id, lmdb::txn &txn)
 {
-        std::string_view keys;
+    std::string_view keys;
 
-        try {
-                auto db  = getUserKeysDb(txn);
-                auto res = db.get(txn, user_id, keys);
+    try {
+        auto db  = getUserKeysDb(txn);
+        auto res = db.get(txn, user_id, keys);
 
-                if (res) {
-                        return json::parse(keys).get<UserKeyCache>();
-                } else {
-                        return {};
-                }
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
-                return {};
+        if (res) {
+            return json::parse(keys).get<UserKeyCache>();
+        } else {
+            return {};
         }
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
+        return {};
+    }
 }
 
 void
 Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
 {
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getUserKeysDb(txn);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getUserKeysDb(txn);
 
-        std::map<std::string, UserKeyCache> updates;
-
-        for (const auto &[user, keys] : keyQuery.device_keys)
-                updates[user].device_keys = keys;
-        for (const auto &[user, keys] : keyQuery.master_keys)
-                updates[user].master_keys = keys;
-        for (const auto &[user, keys] : keyQuery.user_signing_keys)
-                updates[user].user_signing_keys = keys;
-        for (const auto &[user, keys] : keyQuery.self_signing_keys)
-                updates[user].self_signing_keys = keys;
-
-        for (auto &[user, update] : updates) {
-                nhlog::db()->debug("Updated user keys: {}", user);
-
-                auto updateToWrite = update;
-
-                std::string_view oldKeys;
-                auto res = db.get(txn, user, oldKeys);
-
-                if (res) {
-                        updateToWrite     = json::parse(oldKeys).get<UserKeyCache>();
-                        auto last_changed = updateToWrite.last_changed;
-                        // skip if we are tracking this and expect it to be up to date with the last
-                        // sync token
-                        if (!last_changed.empty() && last_changed != sync_token) {
-                                nhlog::db()->debug("Not storing update for user {}, because "
-                                                   "last_changed {}, but we fetched update for {}",
-                                                   user,
-                                                   last_changed,
-                                                   sync_token);
-                                continue;
-                        }
+    std::map<std::string, UserKeyCache> updates;
+
+    for (const auto &[user, keys] : keyQuery.device_keys)
+        updates[user].device_keys = keys;
+    for (const auto &[user, keys] : keyQuery.master_keys)
+        updates[user].master_keys = keys;
+    for (const auto &[user, keys] : keyQuery.user_signing_keys)
+        updates[user].user_signing_keys = keys;
+    for (const auto &[user, keys] : keyQuery.self_signing_keys)
+        updates[user].self_signing_keys = keys;
+
+    for (auto &[user, update] : updates) {
+        nhlog::db()->debug("Updated user keys: {}", user);
+
+        auto updateToWrite = update;
+
+        std::string_view oldKeys;
+        auto res = db.get(txn, user, oldKeys);
 
-                        if (!updateToWrite.master_keys.keys.empty() &&
-                            update.master_keys.keys != updateToWrite.master_keys.keys) {
-                                nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}",
-                                                   user,
-                                                   updateToWrite.master_keys.keys.size(),
-                                                   update.master_keys.keys.size());
-                                updateToWrite.master_key_changed = true;
+        if (res) {
+            updateToWrite     = json::parse(oldKeys).get<UserKeyCache>();
+            auto last_changed = updateToWrite.last_changed;
+            // skip if we are tracking this and expect it to be up to date with the last
+            // sync token
+            if (!last_changed.empty() && last_changed != sync_token) {
+                nhlog::db()->debug("Not storing update for user {}, because "
+                                   "last_changed {}, but we fetched update for {}",
+                                   user,
+                                   last_changed,
+                                   sync_token);
+                continue;
+            }
+
+            if (!updateToWrite.master_keys.keys.empty() &&
+                update.master_keys.keys != updateToWrite.master_keys.keys) {
+                nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}",
+                                   user,
+                                   updateToWrite.master_keys.keys.size(),
+                                   update.master_keys.keys.size());
+                updateToWrite.master_key_changed = true;
+            }
+
+            updateToWrite.master_keys       = update.master_keys;
+            updateToWrite.self_signing_keys = update.self_signing_keys;
+            updateToWrite.user_signing_keys = update.user_signing_keys;
+
+            auto oldDeviceKeys = std::move(updateToWrite.device_keys);
+            updateToWrite.device_keys.clear();
+
+            // Don't insert keys, which we have seen once already
+            for (const auto &[device_id, device_keys] : update.device_keys) {
+                if (oldDeviceKeys.count(device_id) &&
+                    oldDeviceKeys.at(device_id).keys == device_keys.keys) {
+                    // this is safe, since the keys are the same
+                    updateToWrite.device_keys[device_id] = device_keys;
+                } else {
+                    bool keyReused = false;
+                    for (const auto &[key_id, key] : device_keys.keys) {
+                        (void)key_id;
+                        if (updateToWrite.seen_device_keys.count(key)) {
+                            nhlog::crypto()->warn(
+                              "Key '{}' reused by ({}: {})", key, user, device_id);
+                            keyReused = true;
+                            break;
+                        }
+                        if (updateToWrite.seen_device_ids.count(device_id)) {
+                            nhlog::crypto()->warn("device_id '{}' reused by ({})", device_id, user);
+                            keyReused = true;
+                            break;
+                        }
+                    }
+
+                    if (!keyReused && !oldDeviceKeys.count(device_id)) {
+                        // ensure the key has a valid signature from itself
+                        std::string device_signing_key = "ed25519:" + device_keys.device_id;
+                        if (device_id != device_keys.device_id) {
+                            nhlog::crypto()->warn("device {}:{} has a different device id "
+                                                  "in the body: {}",
+                                                  user,
+                                                  device_id,
+                                                  device_keys.device_id);
+                            continue;
+                        }
+                        if (!device_keys.signatures.count(user) ||
+                            !device_keys.signatures.at(user).count(device_signing_key)) {
+                            nhlog::crypto()->warn("device {}:{} has no signature", user, device_id);
+                            continue;
                         }
 
-                        updateToWrite.master_keys       = update.master_keys;
-                        updateToWrite.self_signing_keys = update.self_signing_keys;
-                        updateToWrite.user_signing_keys = update.user_signing_keys;
-
-                        auto oldDeviceKeys = std::move(updateToWrite.device_keys);
-                        updateToWrite.device_keys.clear();
-
-                        // Don't insert keys, which we have seen once already
-                        for (const auto &[device_id, device_keys] : update.device_keys) {
-                                if (oldDeviceKeys.count(device_id) &&
-                                    oldDeviceKeys.at(device_id).keys == device_keys.keys) {
-                                        // this is safe, since the keys are the same
-                                        updateToWrite.device_keys[device_id] = device_keys;
-                                } else {
-                                        bool keyReused = false;
-                                        for (const auto &[key_id, key] : device_keys.keys) {
-                                                (void)key_id;
-                                                if (updateToWrite.seen_device_keys.count(key)) {
-                                                        nhlog::crypto()->warn(
-                                                          "Key '{}' reused by ({}: {})",
-                                                          key,
-                                                          user,
-                                                          device_id);
-                                                        keyReused = true;
-                                                        break;
-                                                }
-                                                if (updateToWrite.seen_device_ids.count(
-                                                      device_id)) {
-                                                        nhlog::crypto()->warn(
-                                                          "device_id '{}' reused by ({})",
-                                                          device_id,
-                                                          user);
-                                                        keyReused = true;
-                                                        break;
-                                                }
-                                        }
-
-                                        if (!keyReused && !oldDeviceKeys.count(device_id)) {
-                                                // ensure the key has a valid signature from itself
-                                                std::string device_signing_key =
-                                                  "ed25519:" + device_keys.device_id;
-                                                if (device_id != device_keys.device_id) {
-                                                        nhlog::crypto()->warn(
-                                                          "device {}:{} has a different device id "
-                                                          "in the body: {}",
-                                                          user,
-                                                          device_id,
-                                                          device_keys.device_id);
-                                                        continue;
-                                                }
-                                                if (!device_keys.signatures.count(user) ||
-                                                    !device_keys.signatures.at(user).count(
-                                                      device_signing_key)) {
-                                                        nhlog::crypto()->warn(
-                                                          "device {}:{} has no signature",
-                                                          user,
-                                                          device_id);
-                                                        continue;
-                                                }
-
-                                                if (!mtx::crypto::ed25519_verify_signature(
-                                                      device_keys.keys.at(device_signing_key),
-                                                      json(device_keys),
-                                                      device_keys.signatures.at(user).at(
-                                                        device_signing_key))) {
-                                                        nhlog::crypto()->warn(
-                                                          "device {}:{} has an invalid signature",
-                                                          user,
-                                                          device_id);
-                                                        continue;
-                                                }
-
-                                                updateToWrite.device_keys[device_id] = device_keys;
-                                        }
-                                }
-
-                                for (const auto &[key_id, key] : device_keys.keys) {
-                                        (void)key_id;
-                                        updateToWrite.seen_device_keys.insert(key);
-                                }
-                                updateToWrite.seen_device_ids.insert(device_id);
+                        if (!mtx::crypto::ed25519_verify_signature(
+                              device_keys.keys.at(device_signing_key),
+                              json(device_keys),
+                              device_keys.signatures.at(user).at(device_signing_key))) {
+                            nhlog::crypto()->warn(
+                              "device {}:{} has an invalid signature", user, device_id);
+                            continue;
                         }
+
+                        updateToWrite.device_keys[device_id] = device_keys;
+                    }
+                }
+
+                for (const auto &[key_id, key] : device_keys.keys) {
+                    (void)key_id;
+                    updateToWrite.seen_device_keys.insert(key);
                 }
-                updateToWrite.updated_at = sync_token;
-                db.put(txn, user, json(updateToWrite).dump());
+                updateToWrite.seen_device_ids.insert(device_id);
+            }
         }
+        updateToWrite.updated_at = sync_token;
+        db.put(txn, user, json(updateToWrite).dump());
+    }
 
-        txn.commit();
+    txn.commit();
 
-        std::map<std::string, VerificationStatus> tmp;
-        const auto local_user = utils::localUser().toStdString();
+    std::map<std::string, VerificationStatus> tmp;
+    const auto local_user = utils::localUser().toStdString();
 
-        {
-                std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
-                for (auto &[user_id, update] : updates) {
-                        (void)update;
-                        if (user_id == local_user) {
-                                std::swap(tmp, verification_storage.status);
-                        } else {
-                                verification_storage.status.erase(user_id);
-                        }
-                }
+    {
+        std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
+        for (auto &[user_id, update] : updates) {
+            (void)update;
+            if (user_id == local_user) {
+                std::swap(tmp, verification_storage.status);
+            } else {
+                verification_storage.status.erase(user_id);
+            }
         }
+    }
 
-        for (auto &[user_id, update] : updates) {
-                (void)update;
-                if (user_id == local_user) {
-                        for (const auto &[user, status] : tmp) {
-                                (void)status;
-                                emit verificationStatusChanged(user);
-                        }
-                }
-                emit verificationStatusChanged(user_id);
+    for (auto &[user_id, update] : updates) {
+        (void)update;
+        if (user_id == local_user) {
+            for (const auto &[user, status] : tmp) {
+                (void)status;
+                emit verificationStatusChanged(user);
+            }
         }
+        emit verificationStatusChanged(user_id);
+    }
 }
 
 void
-Cache::deleteUserKeys(lmdb::txn &txn, lmdb::dbi &db, const std::vector<std::string> &user_ids)
+Cache::markUserKeysOutOfDate(const std::vector<std::string> &user_ids)
 {
-        for (const auto &user_id : user_ids)
-                db.del(txn, user_id);
+    auto currentBatchToken = nextBatchToken();
+    auto txn               = lmdb::txn::begin(env_);
+    auto db                = getUserKeysDb(txn);
+    markUserKeysOutOfDate(txn, db, user_ids, currentBatchToken);
+    txn.commit();
 }
 
 void
@@ -4002,752 +4087,771 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn,
                              const std::vector<std::string> &user_ids,
                              const std::string &sync_token)
 {
-        mtx::requests::QueryKeys query;
-        query.token = sync_token;
-
-        for (const auto &user : user_ids) {
-                nhlog::db()->debug("Marking user keys out of date: {}", user);
-
-                std::string_view oldKeys;
+    mtx::requests::QueryKeys query;
+    query.token = sync_token;
 
-                UserKeyCache cacheEntry;
-                auto res = db.get(txn, user, oldKeys);
-                if (res) {
-                        cacheEntry = json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
-                                       .get<UserKeyCache>();
-                }
-                cacheEntry.last_changed = sync_token;
+    for (const auto &user : user_ids) {
+        nhlog::db()->debug("Marking user keys out of date: {}", user);
 
-                db.put(txn, user, json(cacheEntry).dump());
+        std::string_view oldKeys;
 
-                query.device_keys[user] = {};
+        UserKeyCache cacheEntry;
+        auto res = db.get(txn, user, oldKeys);
+        if (res) {
+            cacheEntry =
+              json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>();
         }
+        cacheEntry.last_changed = sync_token;
 
-        if (!query.device_keys.empty())
-                http::client()->query_keys(query,
-                                           [this, sync_token](const mtx::responses::QueryKeys &keys,
-                                                              mtx::http::RequestErr err) {
-                                                   if (err) {
-                                                           nhlog::net()->warn(
-                                                             "failed to query device keys: {} {}",
-                                                             err->matrix_error.error,
-                                                             static_cast<int>(err->status_code));
-                                                           return;
-                                                   }
+        db.put(txn, user, json(cacheEntry).dump());
 
-                                                   emit userKeysUpdate(sync_token, keys);
-                                           });
+        query.device_keys[user] = {};
+    }
+
+    if (!query.device_keys.empty())
+        http::client()->query_keys(
+          query,
+          [this, sync_token](const mtx::responses::QueryKeys &keys, mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::net()->warn("failed to query device keys: {} {}",
+                                     err->matrix_error.error,
+                                     static_cast<int>(err->status_code));
+                  return;
+              }
+
+              emit userKeysUpdate(sync_token, keys);
+          });
 }
 
 void
 Cache::query_keys(const std::string &user_id,
                   std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb)
 {
-        mtx::requests::QueryKeys req;
-        std::string last_changed;
-        {
-                auto txn    = ro_txn(env_);
-                auto cache_ = userKeys_(user_id, txn);
-
-                if (cache_.has_value()) {
-                        if (cache_->updated_at == cache_->last_changed) {
-                                cb(cache_.value(), {});
-                                return;
-                        } else
-                                nhlog::db()->info("Keys outdated for {}: {} vs {}",
-                                                  user_id,
-                                                  cache_->updated_at,
-                                                  cache_->last_changed);
-                } else
-                        nhlog::db()->info("No keys found for {}", user_id);
-
-                req.device_keys[user_id] = {};
-
-                if (cache_)
-                        last_changed = cache_->last_changed;
-                req.token = last_changed;
-        }
-
-        // use context object so that we can disconnect again
-        QObject *context{new QObject(this)};
-        QObject::connect(
-          this,
-          &Cache::verificationStatusChanged,
-          context,
-          [cb, user_id, context_ = context, this](std::string updated_user) mutable {
-                  if (user_id == updated_user) {
-                          context_->deleteLater();
-                          auto txn  = ro_txn(env_);
-                          auto keys = this->userKeys_(user_id, txn);
-                          cb(keys.value_or(UserKeyCache{}), {});
-                  }
-          },
-          Qt::QueuedConnection);
+    mtx::requests::QueryKeys req;
+    std::string last_changed;
+    {
+        auto txn    = ro_txn(env_);
+        auto cache_ = userKeys_(user_id, txn);
 
-        http::client()->query_keys(
-          req,
-          [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res,
-                                            mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to query device keys: {},{}",
-                                             mtx::errors::to_string(err->matrix_error.errcode),
-                                             static_cast<int>(err->status_code));
-                          cb({}, err);
-                          return;
-                  }
-
-                  emit userKeysUpdate(last_changed, res);
-          });
+        if (cache_.has_value()) {
+            if (cache_->updated_at == cache_->last_changed) {
+                cb(cache_.value(), {});
+                return;
+            } else
+                nhlog::db()->info("Keys outdated for {}: {} vs {}",
+                                  user_id,
+                                  cache_->updated_at,
+                                  cache_->last_changed);
+        } else
+            nhlog::db()->info("No keys found for {}", user_id);
+
+        req.device_keys[user_id] = {};
+
+        if (cache_)
+            last_changed = cache_->last_changed;
+        req.token = last_changed;
+    }
+
+    // use context object so that we can disconnect again
+    QObject *context{new QObject(this)};
+    QObject::connect(
+      this,
+      &Cache::verificationStatusChanged,
+      context,
+      [cb, user_id, context_ = context, this](std::string updated_user) mutable {
+          if (user_id == updated_user) {
+              context_->deleteLater();
+              auto txn  = ro_txn(env_);
+              auto keys = this->userKeys_(user_id, txn);
+              cb(keys.value_or(UserKeyCache{}), {});
+          }
+      },
+      Qt::QueuedConnection);
+
+    http::client()->query_keys(
+      req,
+      [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res,
+                                        mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to query device keys: {},{}",
+                                 mtx::errors::to_string(err->matrix_error.errcode),
+                                 static_cast<int>(err->status_code));
+              cb({}, err);
+              return;
+          }
+
+          emit userKeysUpdate(last_changed, res);
+      });
 }
 
 void
 to_json(json &j, const VerificationCache &info)
 {
-        j["device_verified"] = info.device_verified;
-        j["device_blocked"]  = info.device_blocked;
+    j["device_verified"] = info.device_verified;
+    j["device_blocked"]  = info.device_blocked;
 }
 
 void
 from_json(const json &j, VerificationCache &info)
 {
-        info.device_verified = j.at("device_verified").get<std::set<std::string>>();
-        info.device_blocked  = j.at("device_blocked").get<std::set<std::string>>();
+    info.device_verified = j.at("device_verified").get<std::set<std::string>>();
+    info.device_blocked  = j.at("device_blocked").get<std::set<std::string>>();
+}
+
+void
+to_json(json &j, const OnlineBackupVersion &info)
+{
+    j["v"] = info.version;
+    j["a"] = info.algorithm;
+}
+
+void
+from_json(const json &j, OnlineBackupVersion &info)
+{
+    info.version   = j.at("v").get<std::string>();
+    info.algorithm = j.at("a").get<std::string>();
 }
 
 std::optional<VerificationCache>
 Cache::verificationCache(const std::string &user_id, lmdb::txn &txn)
 {
-        std::string_view verifiedVal;
+    std::string_view verifiedVal;
 
-        auto db = getVerificationDb(txn);
+    auto db = getVerificationDb(txn);
 
-        try {
-                VerificationCache verified_state;
-                auto res = db.get(txn, user_id, verifiedVal);
-                if (res) {
-                        verified_state = json::parse(verifiedVal);
-                        return verified_state;
-                } else {
-                        return {};
-                }
-        } catch (std::exception &) {
-                return {};
+    try {
+        VerificationCache verified_state;
+        auto res = db.get(txn, user_id, verifiedVal);
+        if (res) {
+            verified_state = json::parse(verifiedVal);
+            return verified_state;
+        } else {
+            return {};
         }
+    } catch (std::exception &) {
+        return {};
+    }
 }
 
 void
 Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
 {
-        {
-                std::string_view val;
-
-                auto txn = lmdb::txn::begin(env_);
-                auto db  = getVerificationDb(txn);
-
-                try {
-                        VerificationCache verified_state;
-                        auto res = db.get(txn, user_id, val);
-                        if (res) {
-                                verified_state = json::parse(val);
-                        }
+    {
+        std::string_view val;
 
-                        for (const auto &device : verified_state.device_verified)
-                                if (device == key)
-                                        return;
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getVerificationDb(txn);
 
-                        verified_state.device_verified.insert(key);
-                        db.put(txn, user_id, json(verified_state).dump());
-                        txn.commit();
-                } catch (std::exception &) {
-                }
+        try {
+            VerificationCache verified_state;
+            auto res = db.get(txn, user_id, val);
+            if (res) {
+                verified_state = json::parse(val);
+            }
+
+            for (const auto &device : verified_state.device_verified)
+                if (device == key)
+                    return;
+
+            verified_state.device_verified.insert(key);
+            db.put(txn, user_id, json(verified_state).dump());
+            txn.commit();
+        } catch (std::exception &) {
         }
+    }
 
-        const auto local_user = utils::localUser().toStdString();
-        std::map<std::string, VerificationStatus> tmp;
-        {
-                std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
-                if (user_id == local_user) {
-                        std::swap(tmp, verification_storage.status);
-                } else {
-                        verification_storage.status.erase(user_id);
-                }
-        }
+    const auto local_user = utils::localUser().toStdString();
+    std::map<std::string, VerificationStatus> tmp;
+    {
+        std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
         if (user_id == local_user) {
-                for (const auto &[user, status] : tmp) {
-                        (void)status;
-                        emit verificationStatusChanged(user);
-                }
+            std::swap(tmp, verification_storage.status);
+            verification_storage.status.clear();
         } else {
-                emit verificationStatusChanged(user_id);
+            verification_storage.status.erase(user_id);
         }
+    }
+    if (user_id == local_user) {
+        for (const auto &[user, status] : tmp) {
+            (void)status;
+            emit verificationStatusChanged(user);
+        }
+    }
+    emit verificationStatusChanged(user_id);
 }
 
 void
 Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
 {
-        std::string_view val;
+    std::string_view val;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getVerificationDb(txn);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getVerificationDb(txn);
 
-        try {
-                VerificationCache verified_state;
-                auto res = db.get(txn, user_id, val);
-                if (res) {
-                        verified_state = json::parse(val);
-                }
+    try {
+        VerificationCache verified_state;
+        auto res = db.get(txn, user_id, val);
+        if (res) {
+            verified_state = json::parse(val);
+        }
 
-                verified_state.device_verified.erase(key);
+        verified_state.device_verified.erase(key);
 
-                db.put(txn, user_id, json(verified_state).dump());
-                txn.commit();
-        } catch (std::exception &) {
-        }
+        db.put(txn, user_id, json(verified_state).dump());
+        txn.commit();
+    } catch (std::exception &) {
+    }
 
-        const auto local_user = utils::localUser().toStdString();
-        std::map<std::string, VerificationStatus> tmp;
-        {
-                std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
-                if (user_id == local_user) {
-                        std::swap(tmp, verification_storage.status);
-                } else {
-                        verification_storage.status.erase(user_id);
-                }
-        }
+    const auto local_user = utils::localUser().toStdString();
+    std::map<std::string, VerificationStatus> tmp;
+    {
+        std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
         if (user_id == local_user) {
-                for (const auto &[user, status] : tmp) {
-                        (void)status;
-                        emit verificationStatusChanged(user);
-                }
+            std::swap(tmp, verification_storage.status);
         } else {
-                emit verificationStatusChanged(user_id);
+            verification_storage.status.erase(user_id);
         }
+    }
+    if (user_id == local_user) {
+        for (const auto &[user, status] : tmp) {
+            (void)status;
+            emit verificationStatusChanged(user);
+        }
+    }
+    emit verificationStatusChanged(user_id);
 }
 
 VerificationStatus
 Cache::verificationStatus(const std::string &user_id)
 {
-        auto txn = ro_txn(env_);
-        return verificationStatus_(user_id, txn);
+    auto txn = ro_txn(env_);
+    return verificationStatus_(user_id, txn);
 }
 
 VerificationStatus
 Cache::verificationStatus_(const std::string &user_id, lmdb::txn &txn)
 {
-        std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
-        if (verification_storage.status.count(user_id))
-                return verification_storage.status.at(user_id);
+    std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
+    if (verification_storage.status.count(user_id))
+        return verification_storage.status.at(user_id);
 
-        VerificationStatus status;
+    VerificationStatus status;
 
-        // assume there is at least one unverified device until we have checked we have the device
-        // list for that user.
-        status.unverified_device_count = 1;
-        status.no_keys                 = true;
+    // assume there is at least one unverified device until we have checked we have the device
+    // list for that user.
+    status.unverified_device_count = 1;
+    status.no_keys                 = true;
 
-        if (auto verifCache = verificationCache(user_id, txn)) {
-                status.verified_devices = verifCache->device_verified;
-        }
+    if (auto verifCache = verificationCache(user_id, txn)) {
+        status.verified_devices = verifCache->device_verified;
+    }
 
-        const auto local_user = utils::localUser().toStdString();
+    const auto local_user = utils::localUser().toStdString();
 
-        crypto::Trust trustlevel = crypto::Trust::Unverified;
-        if (user_id == local_user) {
-                status.verified_devices.insert(http::client()->device_id());
-                trustlevel = crypto::Trust::Verified;
-        }
+    crypto::Trust trustlevel = crypto::Trust::Unverified;
+    if (user_id == local_user) {
+        status.verified_devices.insert(http::client()->device_id());
+        trustlevel = crypto::Trust::Verified;
+    }
 
-        auto verifyAtLeastOneSig = [](const auto &toVerif,
-                                      const std::map<std::string, std::string> &keys,
-                                      const std::string &keyOwner) {
-                if (!toVerif.signatures.count(keyOwner))
-                        return false;
+    auto verifyAtLeastOneSig = [](const auto &toVerif,
+                                  const std::map<std::string, std::string> &keys,
+                                  const std::string &keyOwner) {
+        if (!toVerif.signatures.count(keyOwner))
+            return false;
 
-                for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
-                        if (!keys.count(key_id))
-                                continue;
+        for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
+            if (!keys.count(key_id))
+                continue;
 
-                        if (mtx::crypto::ed25519_verify_signature(
-                              keys.at(key_id), json(toVerif), signature))
-                                return true;
-                }
-                return false;
-        };
+            if (mtx::crypto::ed25519_verify_signature(keys.at(key_id), json(toVerif), signature))
+                return true;
+        }
+        return false;
+    };
+
+    auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) {
+        int currentVerifiedDevices = 0;
+        for (auto device_id : status.verified_devices) {
+            if (theirDeviceKeys.count(device_id))
+                currentVerifiedDevices++;
+        }
+        status.unverified_device_count =
+          static_cast<int>(theirDeviceKeys.size()) - currentVerifiedDevices;
+    };
+
+    try {
+        // for local user verify this device_key -> our master_key -> our self_signing_key
+        // -> our device_keys
+        //
+        // for other user verify this device_key -> our master_key -> our user_signing_key
+        // -> their master_key -> their self_signing_key -> their device_keys
+        //
+        // This means verifying the other user adds 2 extra steps,verifying our user_signing
+        // key and their master key
+        auto ourKeys   = userKeys_(local_user, txn);
+        auto theirKeys = userKeys_(user_id, txn);
+        if (theirKeys)
+            status.no_keys = false;
+
+        if (!ourKeys || !theirKeys) {
+            verification_storage.status[user_id] = status;
+            return status;
+        }
+
+        // Update verified devices count to count without cross-signing
+        updateUnverifiedDevices(theirKeys->device_keys);
 
-        auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) {
-                int currentVerifiedDevices = 0;
-                for (auto device_id : status.verified_devices) {
-                        if (theirDeviceKeys.count(device_id))
-                                currentVerifiedDevices++;
-                }
-                status.unverified_device_count =
-                  static_cast<int>(theirDeviceKeys.size()) - currentVerifiedDevices;
-        };
+        {
+            auto &mk           = ourKeys->master_keys;
+            std::string dev_id = "ed25519:" + http::client()->device_id();
+            if (!mk.signatures.count(local_user) || !mk.signatures.at(local_user).count(dev_id) ||
+                !mtx::crypto::ed25519_verify_signature(olm::client()->identity_keys().ed25519,
+                                                       json(mk),
+                                                       mk.signatures.at(local_user).at(dev_id))) {
+                nhlog::crypto()->debug("We have not verified our own master key");
+                verification_storage.status[user_id] = status;
+                return status;
+            }
+        }
 
-        try {
-                // for local user verify this device_key -> our master_key -> our self_signing_key
-                // -> our device_keys
-                //
-                // for other user verify this device_key -> our master_key -> our user_signing_key
-                // -> their master_key -> their self_signing_key -> their device_keys
-                //
-                // This means verifying the other user adds 2 extra steps,verifying our user_signing
-                // key and their master key
-                auto ourKeys   = userKeys_(local_user, txn);
-                auto theirKeys = userKeys_(user_id, txn);
-                if (theirKeys)
-                        status.no_keys = false;
-
-                if (!ourKeys || !theirKeys) {
-                        verification_storage.status[user_id] = status;
-                        return status;
-                }
+        auto master_keys = ourKeys->master_keys.keys;
 
-                // Update verified devices count to count without cross-signing
-                updateUnverifiedDevices(theirKeys->device_keys);
+        if (user_id != local_user) {
+            bool theirMasterKeyVerified =
+              verifyAtLeastOneSig(ourKeys->user_signing_keys, master_keys, local_user) &&
+              verifyAtLeastOneSig(
+                theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user);
 
-                if (!mtx::crypto::ed25519_verify_signature(
-                      olm::client()->identity_keys().ed25519,
-                      json(ourKeys->master_keys),
-                      ourKeys->master_keys.signatures.at(local_user)
-                        .at("ed25519:" + http::client()->device_id()))) {
-                        verification_storage.status[user_id] = status;
-                        return status;
-                }
+            if (theirMasterKeyVerified)
+                trustlevel = crypto::Trust::Verified;
+            else if (!theirKeys->master_key_changed)
+                trustlevel = crypto::Trust::TOFU;
+            else {
+                verification_storage.status[user_id] = status;
+                return status;
+            }
 
-                auto master_keys = ourKeys->master_keys.keys;
-
-                if (user_id != local_user) {
-                        bool theirMasterKeyVerified =
-                          verifyAtLeastOneSig(
-                            ourKeys->user_signing_keys, master_keys, local_user) &&
-                          verifyAtLeastOneSig(
-                            theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user);
-
-                        if (theirMasterKeyVerified)
-                                trustlevel = crypto::Trust::Verified;
-                        else if (!theirKeys->master_key_changed)
-                                trustlevel = crypto::Trust::TOFU;
-                        else {
-                                verification_storage.status[user_id] = status;
-                                return status;
-                        }
+            master_keys = theirKeys->master_keys.keys;
+        }
 
-                        master_keys = theirKeys->master_keys.keys;
-                }
+        status.user_verified = trustlevel;
 
-                status.user_verified = trustlevel;
+        verification_storage.status[user_id] = status;
+        if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
+            return status;
 
-                verification_storage.status[user_id] = status;
-                if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
-                        return status;
-
-                for (const auto &[device, device_key] : theirKeys->device_keys) {
-                        (void)device;
-                        try {
-                                auto identkey =
-                                  device_key.keys.at("curve25519:" + device_key.device_id);
-                                if (verifyAtLeastOneSig(
-                                      device_key, theirKeys->self_signing_keys.keys, user_id)) {
-                                        status.verified_devices.insert(device_key.device_id);
-                                        status.verified_device_keys[identkey] = trustlevel;
-                                }
-                        } catch (...) {
-                        }
+        for (const auto &[device, device_key] : theirKeys->device_keys) {
+            (void)device;
+            try {
+                auto identkey = device_key.keys.at("curve25519:" + device_key.device_id);
+                if (verifyAtLeastOneSig(device_key, theirKeys->self_signing_keys.keys, user_id)) {
+                    status.verified_devices.insert(device_key.device_id);
+                    status.verified_device_keys[identkey] = trustlevel;
                 }
-
-                updateUnverifiedDevices(theirKeys->device_keys);
-                verification_storage.status[user_id] = status;
-                return status;
-        } catch (std::exception &e) {
-                nhlog::db()->error(
-                  "Failed to calculate verification status of {}: {}", user_id, e.what());
-                return status;
+            } catch (...) {
+            }
         }
+
+        updateUnverifiedDevices(theirKeys->device_keys);
+        verification_storage.status[user_id] = status;
+        return status;
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to calculate verification status of {}: {}", user_id, e.what());
+        return status;
+    }
 }
 
 void
 to_json(json &j, const RoomInfo &info)
 {
-        j["name"]         = info.name;
-        j["topic"]        = info.topic;
-        j["avatar_url"]   = info.avatar_url;
-        j["version"]      = info.version;
-        j["is_invite"]    = info.is_invite;
-        j["is_space"]     = info.is_space;
-        j["join_rule"]    = info.join_rule;
-        j["guest_access"] = info.guest_access;
+    j["name"]         = info.name;
+    j["topic"]        = info.topic;
+    j["avatar_url"]   = info.avatar_url;
+    j["version"]      = info.version;
+    j["is_invite"]    = info.is_invite;
+    j["is_space"]     = info.is_space;
+    j["join_rule"]    = info.join_rule;
+    j["guest_access"] = info.guest_access;
 
-        if (info.member_count != 0)
-                j["member_count"] = info.member_count;
+    if (info.member_count != 0)
+        j["member_count"] = info.member_count;
 
-        if (info.tags.size() != 0)
-                j["tags"] = info.tags;
+    if (info.tags.size() != 0)
+        j["tags"] = info.tags;
 }
 
 void
 from_json(const json &j, RoomInfo &info)
 {
-        info.name       = j.at("name");
-        info.topic      = j.at("topic");
-        info.avatar_url = j.at("avatar_url");
-        info.version    = j.value(
-          "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString());
-        info.is_invite    = j.at("is_invite");
-        info.is_space     = j.value("is_space", false);
-        info.join_rule    = j.at("join_rule");
-        info.guest_access = j.at("guest_access");
+    info.name       = j.at("name");
+    info.topic      = j.at("topic");
+    info.avatar_url = j.at("avatar_url");
+    info.version    = j.value(
+      "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString());
+    info.is_invite    = j.at("is_invite");
+    info.is_space     = j.value("is_space", false);
+    info.join_rule    = j.at("join_rule");
+    info.guest_access = j.at("guest_access");
 
-        if (j.count("member_count"))
-                info.member_count = j.at("member_count");
+    if (j.count("member_count"))
+        info.member_count = j.at("member_count");
 
-        if (j.count("tags"))
-                info.tags = j.at("tags").get<std::vector<std::string>>();
+    if (j.count("tags"))
+        info.tags = j.at("tags").get<std::vector<std::string>>();
 }
 
 void
 to_json(json &j, const ReadReceiptKey &key)
 {
-        j = json{{"event_id", key.event_id}, {"room_id", key.room_id}};
+    j = json{{"event_id", key.event_id}, {"room_id", key.room_id}};
 }
 
 void
 from_json(const json &j, ReadReceiptKey &key)
 {
-        key.event_id = j.at("event_id").get<std::string>();
-        key.room_id  = j.at("room_id").get<std::string>();
+    key.event_id = j.at("event_id").get<std::string>();
+    key.room_id  = j.at("room_id").get<std::string>();
 }
 
 void
 to_json(json &j, const MemberInfo &info)
 {
-        j["name"]       = info.name;
-        j["avatar_url"] = info.avatar_url;
+    j["name"]       = info.name;
+    j["avatar_url"] = info.avatar_url;
 }
 
 void
 from_json(const json &j, MemberInfo &info)
 {
-        info.name       = j.at("name");
-        info.avatar_url = j.at("avatar_url");
+    info.name       = j.at("name");
+    info.avatar_url = j.at("avatar_url");
 }
 
 void
 to_json(nlohmann::json &obj, const DeviceKeysToMsgIndex &msg)
 {
-        obj["deviceids"] = msg.deviceids;
+    obj["deviceids"] = msg.deviceids;
 }
 
 void
 from_json(const nlohmann::json &obj, DeviceKeysToMsgIndex &msg)
 {
-        msg.deviceids = obj.at("deviceids").get<decltype(msg.deviceids)>();
+    msg.deviceids = obj.at("deviceids").get<decltype(msg.deviceids)>();
 }
 
 void
 to_json(nlohmann::json &obj, const SharedWithUsers &msg)
 {
-        obj["keys"] = msg.keys;
+    obj["keys"] = msg.keys;
 }
 
 void
 from_json(const nlohmann::json &obj, SharedWithUsers &msg)
 {
-        msg.keys = obj.at("keys").get<std::map<std::string, DeviceKeysToMsgIndex>>();
+    msg.keys = obj.at("keys").get<std::map<std::string, DeviceKeysToMsgIndex>>();
 }
 
 void
 to_json(nlohmann::json &obj, const GroupSessionData &msg)
 {
-        obj["message_index"] = msg.message_index;
-        obj["ts"]            = msg.timestamp;
+    obj["message_index"] = msg.message_index;
+    obj["ts"]            = msg.timestamp;
+    obj["trust"]         = msg.trusted;
 
-        obj["sender_claimed_ed25519_key"]      = msg.sender_claimed_ed25519_key;
-        obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain;
+    obj["sender_claimed_ed25519_key"]      = msg.sender_claimed_ed25519_key;
+    obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain;
 
-        obj["currently"] = msg.currently;
+    obj["currently"] = msg.currently;
 
-        obj["indices"] = msg.indices;
+    obj["indices"] = msg.indices;
 }
 
 void
 from_json(const nlohmann::json &obj, GroupSessionData &msg)
 {
-        msg.message_index = obj.at("message_index");
-        msg.timestamp     = obj.value("ts", 0ULL);
+    msg.message_index = obj.at("message_index");
+    msg.timestamp     = obj.value("ts", 0ULL);
+    msg.trusted       = obj.value("trust", true);
 
-        msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", "");
-        msg.forwarding_curve25519_key_chain =
-          obj.value("forwarding_curve25519_key_chain", std::vector<std::string>{});
+    msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", "");
+    msg.forwarding_curve25519_key_chain =
+      obj.value("forwarding_curve25519_key_chain", std::vector<std::string>{});
 
-        msg.currently = obj.value("currently", SharedWithUsers{});
+    msg.currently = obj.value("currently", SharedWithUsers{});
 
-        msg.indices = obj.value("indices", std::map<uint32_t, std::string>());
+    msg.indices = obj.value("indices", std::map<uint32_t, std::string>());
 }
 
 void
 to_json(nlohmann::json &obj, const DevicePublicKeys &msg)
 {
-        obj["ed25519"]    = msg.ed25519;
-        obj["curve25519"] = msg.curve25519;
+    obj["ed25519"]    = msg.ed25519;
+    obj["curve25519"] = msg.curve25519;
 }
 
 void
 from_json(const nlohmann::json &obj, DevicePublicKeys &msg)
 {
-        msg.ed25519    = obj.at("ed25519");
-        msg.curve25519 = obj.at("curve25519");
+    msg.ed25519    = obj.at("ed25519");
+    msg.curve25519 = obj.at("curve25519");
 }
 
 void
 to_json(nlohmann::json &obj, const MegolmSessionIndex &msg)
 {
-        obj["room_id"]    = msg.room_id;
-        obj["session_id"] = msg.session_id;
-        obj["sender_key"] = msg.sender_key;
+    obj["room_id"]    = msg.room_id;
+    obj["session_id"] = msg.session_id;
+    obj["sender_key"] = msg.sender_key;
 }
 
 void
 from_json(const nlohmann::json &obj, MegolmSessionIndex &msg)
 {
-        msg.room_id    = obj.at("room_id");
-        msg.session_id = obj.at("session_id");
-        msg.sender_key = obj.at("sender_key");
+    msg.room_id    = obj.at("room_id");
+    msg.session_id = obj.at("session_id");
+    msg.sender_key = obj.at("sender_key");
 }
 
 void
 to_json(nlohmann::json &obj, const StoredOlmSession &msg)
 {
-        obj["ts"] = msg.last_message_ts;
-        obj["s"]  = msg.pickled_session;
+    obj["ts"] = msg.last_message_ts;
+    obj["s"]  = msg.pickled_session;
 }
 void
 from_json(const nlohmann::json &obj, StoredOlmSession &msg)
 {
-        msg.last_message_ts = obj.at("ts").get<uint64_t>();
-        msg.pickled_session = obj.at("s").get<std::string>();
+    msg.last_message_ts = obj.at("ts").get<uint64_t>();
+    msg.pickled_session = obj.at("s").get<std::string>();
 }
 
 namespace cache {
 void
 init(const QString &user_id)
 {
-        qRegisterMetaType<RoomMember>();
-        qRegisterMetaType<RoomSearchResult>();
-        qRegisterMetaType<RoomInfo>();
-        qRegisterMetaType<QMap<QString, RoomInfo>>();
-        qRegisterMetaType<std::map<QString, RoomInfo>>();
-        qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>();
-        qRegisterMetaType<mtx::responses::QueryKeys>();
+    qRegisterMetaType<RoomMember>();
+    qRegisterMetaType<RoomSearchResult>();
+    qRegisterMetaType<RoomInfo>();
+    qRegisterMetaType<QMap<QString, RoomInfo>>();
+    qRegisterMetaType<std::map<QString, RoomInfo>>();
+    qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>();
+    qRegisterMetaType<mtx::responses::QueryKeys>();
 
-        instance_ = std::make_unique<Cache>(user_id);
+    instance_ = std::make_unique<Cache>(user_id);
 }
 
 Cache *
 client()
 {
-        return instance_.get();
+    return instance_.get();
 }
 
 std::string
 displayName(const std::string &room_id, const std::string &user_id)
 {
-        return instance_->displayName(room_id, user_id);
+    return instance_->displayName(room_id, user_id);
 }
 
 QString
 displayName(const QString &room_id, const QString &user_id)
 {
-        return instance_->displayName(room_id, user_id);
+    return instance_->displayName(room_id, user_id);
 }
 QString
 avatarUrl(const QString &room_id, const QString &user_id)
 {
-        return instance_->avatarUrl(room_id, user_id);
+    return instance_->avatarUrl(room_id, user_id);
 }
 
 mtx::presence::PresenceState
 presenceState(const std::string &user_id)
 {
-        if (!instance_)
-                return {};
-        return instance_->presenceState(user_id);
+    if (!instance_)
+        return {};
+    return instance_->presenceState(user_id);
 }
 std::string
 statusMessage(const std::string &user_id)
 {
-        return instance_->statusMessage(user_id);
+    return instance_->statusMessage(user_id);
 }
 
 // user cache stores user keys
 std::optional<UserKeyCache>
 userKeys(const std::string &user_id)
 {
-        return instance_->userKeys(user_id);
+    return instance_->userKeys(user_id);
 }
 void
 updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
 {
-        instance_->updateUserKeys(sync_token, keyQuery);
+    instance_->updateUserKeys(sync_token, keyQuery);
 }
 
 // device & user verification cache
 std::optional<VerificationStatus>
 verificationStatus(const std::string &user_id)
 {
-        return instance_->verificationStatus(user_id);
+    return instance_->verificationStatus(user_id);
 }
 
 void
 markDeviceVerified(const std::string &user_id, const std::string &device)
 {
-        instance_->markDeviceVerified(user_id, device);
+    instance_->markDeviceVerified(user_id, device);
 }
 
 void
 markDeviceUnverified(const std::string &user_id, const std::string &device)
 {
-        instance_->markDeviceUnverified(user_id, device);
+    instance_->markDeviceUnverified(user_id, device);
 }
 
 std::vector<std::string>
 joinedRooms()
 {
-        return instance_->joinedRooms();
+    return instance_->joinedRooms();
 }
 
 QMap<QString, RoomInfo>
 roomInfo(bool withInvites)
 {
-        return instance_->roomInfo(withInvites);
+    return instance_->roomInfo(withInvites);
 }
 QHash<QString, RoomInfo>
 invites()
 {
-        return instance_->invites();
+    return instance_->invites();
 }
 
 QString
 getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        return instance_->getRoomName(txn, statesdb, membersdb);
+    return instance_->getRoomName(txn, statesdb, membersdb);
 }
 mtx::events::state::JoinRule
 getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        return instance_->getRoomJoinRule(txn, statesdb);
+    return instance_->getRoomJoinRule(txn, statesdb);
 }
 bool
 getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        return instance_->getRoomGuestAccess(txn, statesdb);
+    return instance_->getRoomGuestAccess(txn, statesdb);
 }
 QString
 getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        return instance_->getRoomTopic(txn, statesdb);
+    return instance_->getRoomTopic(txn, statesdb);
 }
 QString
 getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        return instance_->getRoomAvatarUrl(txn, statesdb, membersdb);
+    return instance_->getRoomAvatarUrl(txn, statesdb, membersdb);
 }
 
 std::vector<RoomMember>
 getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
 {
-        return instance_->getMembers(room_id, startIndex, len);
+    return instance_->getMembers(room_id, startIndex, len);
+}
+
+std::vector<RoomMember>
+getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len)
+{
+    return instance_->getMembersFromInvite(room_id, startIndex, len);
 }
 
 void
 saveState(const mtx::responses::Sync &res)
 {
-        instance_->saveState(res);
+    instance_->saveState(res);
 }
 bool
 isInitialized()
 {
-        return instance_->isInitialized();
+    return instance_->isInitialized();
 }
 
 std::string
 nextBatchToken()
 {
-        return instance_->nextBatchToken();
+    return instance_->nextBatchToken();
 }
 
 void
 deleteData()
 {
-        instance_->deleteData();
+    instance_->deleteData();
 }
 
 void
 removeInvite(lmdb::txn &txn, const std::string &room_id)
 {
-        instance_->removeInvite(txn, room_id);
+    instance_->removeInvite(txn, room_id);
 }
 void
 removeInvite(const std::string &room_id)
 {
-        instance_->removeInvite(room_id);
+    instance_->removeInvite(room_id);
 }
 void
 removeRoom(lmdb::txn &txn, const std::string &roomid)
 {
-        instance_->removeRoom(txn, roomid);
+    instance_->removeRoom(txn, roomid);
 }
 void
 removeRoom(const std::string &roomid)
 {
-        instance_->removeRoom(roomid);
+    instance_->removeRoom(roomid);
 }
 void
 removeRoom(const QString &roomid)
 {
-        instance_->removeRoom(roomid.toStdString());
+    instance_->removeRoom(roomid.toStdString());
 }
 void
 setup()
 {
-        instance_->setup();
+    instance_->setup();
 }
 
 bool
 runMigrations()
 {
-        return instance_->runMigrations();
+    return instance_->runMigrations();
 }
 
 cache::CacheVersion
 formatVersion()
 {
-        return instance_->formatVersion();
+    return instance_->formatVersion();
 }
 
 void
 setCurrentFormat()
 {
-        instance_->setCurrentFormat();
+    instance_->setCurrentFormat();
 }
 
 std::vector<QString>
 roomIds()
 {
-        return instance_->roomIds();
+    return instance_->roomIds();
 }
 
 QMap<QString, mtx::responses::Notifications>
 getTimelineMentions()
 {
-        return instance_->getTimelineMentions();
+    return instance_->getTimelineMentions();
 }
 
 //! Retrieve all the user ids from a room.
 std::vector<std::string>
 roomMembers(const std::string &room_id)
 {
-        return instance_->roomMembers(room_id);
+    return instance_->roomMembers(room_id);
 }
 
 //! Check if the given user has power leve greater than than
@@ -4757,48 +4861,48 @@ hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
                     const std::string &room_id,
                     const std::string &user_id)
 {
-        return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id);
+    return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id);
 }
 
 void
 updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
 {
-        instance_->updateReadReceipt(txn, room_id, receipts);
+    instance_->updateReadReceipt(txn, room_id, receipts);
 }
 
 UserReceipts
 readReceipts(const QString &event_id, const QString &room_id)
 {
-        return instance_->readReceipts(event_id, room_id);
+    return instance_->readReceipts(event_id, room_id);
 }
 
 std::optional<uint64_t>
 getEventIndex(const std::string &room_id, std::string_view event_id)
 {
-        return instance_->getEventIndex(room_id, event_id);
+    return instance_->getEventIndex(room_id, event_id);
 }
 
 std::optional<std::pair<uint64_t, std::string>>
 lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id)
 {
-        return instance_->lastInvisibleEventAfter(room_id, event_id);
+    return instance_->lastInvisibleEventAfter(room_id, event_id);
 }
 
 RoomInfo
 singleRoomInfo(const std::string &room_id)
 {
-        return instance_->singleRoomInfo(room_id);
+    return instance_->singleRoomInfo(room_id);
 }
 std::vector<std::string>
 roomsWithStateUpdates(const mtx::responses::Sync &res)
 {
-        return instance_->roomsWithStateUpdates(res);
+    return instance_->roomsWithStateUpdates(res);
 }
 
 std::map<QString, RoomInfo>
 getRoomInfo(const std::vector<std::string> &rooms)
 {
-        return instance_->getRoomInfo(rooms);
+    return instance_->getRoomInfo(rooms);
 }
 
 //! Calculates which the read status of a room.
@@ -4806,74 +4910,74 @@ getRoomInfo(const std::vector<std::string> &rooms)
 bool
 calculateRoomReadStatus(const std::string &room_id)
 {
-        return instance_->calculateRoomReadStatus(room_id);
+    return instance_->calculateRoomReadStatus(room_id);
 }
 void
 calculateRoomReadStatus()
 {
-        instance_->calculateRoomReadStatus();
+    instance_->calculateRoomReadStatus();
 }
 
 void
 markSentNotification(const std::string &event_id)
 {
-        instance_->markSentNotification(event_id);
+    instance_->markSentNotification(event_id);
 }
 //! Removes an event from the sent notifications.
 void
 removeReadNotification(const std::string &event_id)
 {
-        instance_->removeReadNotification(event_id);
+    instance_->removeReadNotification(event_id);
 }
 //! Check if we have sent a desktop notification for the given event id.
 bool
 isNotificationSent(const std::string &event_id)
 {
-        return instance_->isNotificationSent(event_id);
+    return instance_->isNotificationSent(event_id);
 }
 
 //! Add all notifications containing a user mention to the db.
 void
 saveTimelineMentions(const mtx::responses::Notifications &res)
 {
-        instance_->saveTimelineMentions(res);
+    instance_->saveTimelineMentions(res);
 }
 
 //! Remove old unused data.
 void
 deleteOldMessages()
 {
-        instance_->deleteOldMessages();
+    instance_->deleteOldMessages();
 }
 void
 deleteOldData() noexcept
 {
-        instance_->deleteOldData();
+    instance_->deleteOldData();
 }
 //! Retrieve all saved room ids.
 std::vector<std::string>
 getRoomIds(lmdb::txn &txn)
 {
-        return instance_->getRoomIds(txn);
+    return instance_->getRoomIds(txn);
 }
 
 //! Mark a room that uses e2e encryption.
 void
 setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
 {
-        instance_->setEncryptedRoom(txn, room_id);
+    instance_->setEncryptedRoom(txn, room_id);
 }
 bool
 isRoomEncrypted(const std::string &room_id)
 {
-        return instance_->isRoomEncrypted(room_id);
+    return instance_->isRoomEncrypted(room_id);
 }
 
 //! Check if a user is a member of the room.
 bool
 isRoomMember(const std::string &user_id, const std::string &room_id)
 {
-        return instance_->isRoomMember(user_id, room_id);
+    return instance_->isRoomMember(user_id, room_id);
 }
 
 //
@@ -4884,40 +4988,40 @@ saveOutboundMegolmSession(const std::string &room_id,
                           const GroupSessionData &data,
                           mtx::crypto::OutboundGroupSessionPtr &session)
 {
-        instance_->saveOutboundMegolmSession(room_id, data, session);
+    instance_->saveOutboundMegolmSession(room_id, data, session);
 }
 OutboundGroupSessionDataRef
 getOutboundMegolmSession(const std::string &room_id)
 {
-        return instance_->getOutboundMegolmSession(room_id);
+    return instance_->getOutboundMegolmSession(room_id);
 }
 bool
 outboundMegolmSessionExists(const std::string &room_id) noexcept
 {
-        return instance_->outboundMegolmSessionExists(room_id);
+    return instance_->outboundMegolmSessionExists(room_id);
 }
 void
 updateOutboundMegolmSession(const std::string &room_id,
                             const GroupSessionData &data,
                             mtx::crypto::OutboundGroupSessionPtr &session)
 {
-        instance_->updateOutboundMegolmSession(room_id, data, session);
+    instance_->updateOutboundMegolmSession(room_id, data, session);
 }
 void
 dropOutboundMegolmSession(const std::string &room_id)
 {
-        instance_->dropOutboundMegolmSession(room_id);
+    instance_->dropOutboundMegolmSession(room_id);
 }
 
 void
 importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
 {
-        instance_->importSessionKeys(keys);
+    instance_->importSessionKeys(keys);
 }
 mtx::crypto::ExportedSessionKeys
 exportSessionKeys()
 {
-        return instance_->exportSessionKeys();
+    return instance_->exportSessionKeys();
 }
 
 //
@@ -4928,22 +5032,22 @@ saveInboundMegolmSession(const MegolmSessionIndex &index,
                          mtx::crypto::InboundGroupSessionPtr session,
                          const GroupSessionData &data)
 {
-        instance_->saveInboundMegolmSession(index, std::move(session), data);
+    instance_->saveInboundMegolmSession(index, std::move(session), data);
 }
 mtx::crypto::InboundGroupSessionPtr
 getInboundMegolmSession(const MegolmSessionIndex &index)
 {
-        return instance_->getInboundMegolmSession(index);
+    return instance_->getInboundMegolmSession(index);
 }
 bool
 inboundMegolmSessionExists(const MegolmSessionIndex &index)
 {
-        return instance_->inboundMegolmSessionExists(index);
+    return instance_->inboundMegolmSessionExists(index);
 }
 std::optional<GroupSessionData>
 getMegolmSessionData(const MegolmSessionIndex &index)
 {
-        return instance_->getMegolmSessionData(index);
+    return instance_->getMegolmSessionData(index);
 }
 
 //
@@ -4954,43 +5058,43 @@ saveOlmSession(const std::string &curve25519,
                mtx::crypto::OlmSessionPtr session,
                uint64_t timestamp)
 {
-        instance_->saveOlmSession(curve25519, std::move(session), timestamp);
+    instance_->saveOlmSession(curve25519, std::move(session), timestamp);
 }
 std::vector<std::string>
 getOlmSessions(const std::string &curve25519)
 {
-        return instance_->getOlmSessions(curve25519);
+    return instance_->getOlmSessions(curve25519);
 }
 std::optional<mtx::crypto::OlmSessionPtr>
 getOlmSession(const std::string &curve25519, const std::string &session_id)
 {
-        return instance_->getOlmSession(curve25519, session_id);
+    return instance_->getOlmSession(curve25519, session_id);
 }
 std::optional<mtx::crypto::OlmSessionPtr>
 getLatestOlmSession(const std::string &curve25519)
 {
-        return instance_->getLatestOlmSession(curve25519);
+    return instance_->getLatestOlmSession(curve25519);
 }
 
 void
 saveOlmAccount(const std::string &pickled)
 {
-        instance_->saveOlmAccount(pickled);
+    instance_->saveOlmAccount(pickled);
 }
 std::string
 restoreOlmAccount()
 {
-        return instance_->restoreOlmAccount();
+    return instance_->restoreOlmAccount();
 }
 
 void
 storeSecret(const std::string &name, const std::string &secret)
 {
-        instance_->storeSecret(name, secret);
+    instance_->storeSecret(name, secret);
 }
 std::optional<std::string>
 secret(const std::string &name)
 {
-        return instance_->secret(name);
+    return instance_->secret(name);
 }
 } // namespace cache
diff --git a/src/Cache.h b/src/Cache.h
index 57a36d73aa81794cc0faef159781bf0876ebb2ee..f862643005798a57d4fca3c5175beaf0a2a3ee38 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -83,6 +83,9 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
 //! Retrieve member info from a room.
 std::vector<RoomMember>
 getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30);
+//! Retrive member info from an invite.
+std::vector<RoomMember>
+getMembersFromInvite(const std::string &room_id, std::size_t start_index = 0, std::size_t len = 30);
 
 bool
 isInitialized();
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 6c402674a0d25e12313882164b88183df6939a72..b746184809b32122f899f0a577de9c4d422de5e8 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -19,43 +19,48 @@ Q_NAMESPACE
 //! How much a participant is trusted.
 enum Trust
 {
-        Unverified, //! Device unverified or master key changed.
-        TOFU,       //! Device is signed by the sender, but the user is not verified, but they never
-                    //! changed the master key.
-        Verified,   //! User was verified and has crosssigned this device or device is verified.
+    Unverified, //! Device unverified or master key changed.
+    TOFU,       //! Device is signed by the sender, but the user is not verified, but they never
+                //! changed the master key.
+    Verified,   //! User was verified and has crosssigned this device or device is verified.
 };
 Q_ENUM_NS(Trust)
 }
 
 struct DeviceKeysToMsgIndex
 {
-        // map from device key to message_index
-        // Using the device id is safe because we check for reuse on device list updates
-        // Using the device id makes our logic much easier to read.
-        std::map<std::string, uint64_t> deviceids;
+    // map from device key to message_index
+    // Using the device id is safe because we check for reuse on device list updates
+    // Using the device id makes our logic much easier to read.
+    std::map<std::string, uint64_t> deviceids;
 };
 
 struct SharedWithUsers
 {
-        // userid to keys
-        std::map<std::string, DeviceKeysToMsgIndex> keys;
+    // userid to keys
+    std::map<std::string, DeviceKeysToMsgIndex> keys;
 };
 
 // Extra information associated with an outbound megolm session.
 struct GroupSessionData
 {
-        uint64_t message_index = 0;
-        uint64_t timestamp     = 0;
+    uint64_t message_index = 0;
+    uint64_t timestamp     = 0;
 
-        std::string sender_claimed_ed25519_key;
-        std::vector<std::string> forwarding_curve25519_key_chain;
+    // If we got the session via key sharing or forwarding, we can usually trust it.
+    // If it came from asymmetric key backup, it is not trusted.
+    // TODO(Nico): What about forwards? They might come from key backup?
+    bool trusted = true;
 
-        //! map from index to event_id to check for replay attacks
-        std::map<uint32_t, std::string> indices;
+    std::string sender_claimed_ed25519_key;
+    std::vector<std::string> forwarding_curve25519_key_chain;
 
-        // who has access to this session.
-        // Rotate, when a user leaves the room and share, when a user gets added.
-        SharedWithUsers currently;
+    //! map from index to event_id to check for replay attacks
+    std::map<uint32_t, std::string> indices;
+
+    // who has access to this session.
+    // Rotate, when a user leaves the room and share, when a user gets added.
+    SharedWithUsers currently;
 };
 
 void
@@ -65,14 +70,14 @@ from_json(const nlohmann::json &obj, GroupSessionData &msg);
 
 struct OutboundGroupSessionDataRef
 {
-        mtx::crypto::OutboundGroupSessionPtr session;
-        GroupSessionData data;
+    mtx::crypto::OutboundGroupSessionPtr session;
+    GroupSessionData data;
 };
 
 struct DevicePublicKeys
 {
-        std::string ed25519;
-        std::string curve25519;
+    std::string ed25519;
+    std::string curve25519;
 };
 
 void
@@ -83,12 +88,19 @@ from_json(const nlohmann::json &obj, DevicePublicKeys &msg);
 //! Represents a unique megolm session identifier.
 struct MegolmSessionIndex
 {
-        //! The room in which this session exists.
-        std::string room_id;
-        //! The session_id of the megolm session.
-        std::string session_id;
-        //! The curve25519 public key of the sender.
-        std::string sender_key;
+    MegolmSessionIndex() = default;
+    MegolmSessionIndex(std::string room_id_, const mtx::events::msg::Encrypted &e)
+      : room_id(std::move(room_id_))
+      , session_id(e.session_id)
+      , sender_key(e.sender_key)
+    {}
+
+    //! The room in which this session exists.
+    std::string room_id;
+    //! The session_id of the megolm session.
+    std::string session_id;
+    //! The curve25519 public key of the sender.
+    std::string sender_key;
 };
 
 void
@@ -98,8 +110,8 @@ from_json(const nlohmann::json &obj, MegolmSessionIndex &msg);
 
 struct StoredOlmSession
 {
-        std::uint64_t last_message_ts = 0;
-        std::string pickled_session;
+    std::uint64_t last_message_ts = 0;
+    std::string pickled_session;
 };
 void
 to_json(nlohmann::json &obj, const StoredOlmSession &msg);
@@ -109,43 +121,43 @@ from_json(const nlohmann::json &obj, StoredOlmSession &msg);
 //! Verification status of a single user
 struct VerificationStatus
 {
-        //! True, if the users master key is verified
-        crypto::Trust user_verified = crypto::Trust::Unverified;
-        //! List of all devices marked as verified
-        std::set<std::string> verified_devices;
-        //! Map from sender key/curve25519 to trust status
-        std::map<std::string, crypto::Trust> verified_device_keys;
-        //! Count of unverified devices
-        int unverified_device_count = 0;
-        // if the keys are not in cache
-        bool no_keys = false;
+    //! True, if the users master key is verified
+    crypto::Trust user_verified = crypto::Trust::Unverified;
+    //! List of all devices marked as verified
+    std::set<std::string> verified_devices;
+    //! Map from sender key/curve25519 to trust status
+    std::map<std::string, crypto::Trust> verified_device_keys;
+    //! Count of unverified devices
+    int unverified_device_count = 0;
+    // if the keys are not in cache
+    bool no_keys = false;
 };
 
 //! In memory cache of verification status
 struct VerificationStorage
 {
-        //! mapping of user to verification status
-        std::map<std::string, VerificationStatus> status;
-        std::mutex verification_storage_mtx;
+    //! mapping of user to verification status
+    std::map<std::string, VerificationStatus> status;
+    std::mutex verification_storage_mtx;
 };
 
 // this will store the keys of the user with whom a encrypted room is shared with
 struct UserKeyCache
 {
-        //! Device id to device keys
-        std::map<std::string, mtx::crypto::DeviceKeys> device_keys;
-        //! cross signing keys
-        mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
-        //! Sync token when nheko last fetched the keys
-        std::string updated_at;
-        //! Sync token when the keys last changed. updated != last_changed means they are outdated.
-        std::string last_changed;
-        //! if the master key has ever changed
-        bool master_key_changed = false;
-        //! Device keys that were already used at least once
-        std::set<std::string> seen_device_keys;
-        //! Device ids that were already used at least once
-        std::set<std::string> seen_device_ids;
+    //! Device id to device keys
+    std::map<std::string, mtx::crypto::DeviceKeys> device_keys;
+    //! cross signing keys
+    mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
+    //! Sync token when nheko last fetched the keys
+    std::string updated_at;
+    //! Sync token when the keys last changed. updated != last_changed means they are outdated.
+    std::string last_changed;
+    //! if the master key has ever changed
+    bool master_key_changed = false;
+    //! Device keys that were already used at least once
+    std::set<std::string> seen_device_keys;
+    //! Device ids that were already used at least once
+    std::set<std::string> seen_device_ids;
 };
 
 void
@@ -157,13 +169,26 @@ from_json(const nlohmann::json &j, UserKeyCache &info);
 // UserKeyCache stores only keys of users with which encrypted room is shared
 struct VerificationCache
 {
-        //! list of verified device_ids with device-verification
-        std::set<std::string> device_verified;
-        //! list of devices the user blocks
-        std::set<std::string> device_blocked;
+    //! list of verified device_ids with device-verification
+    std::set<std::string> device_verified;
+    //! list of devices the user blocks
+    std::set<std::string> device_blocked;
 };
 
 void
 to_json(nlohmann::json &j, const VerificationCache &info);
 void
 from_json(const nlohmann::json &j, VerificationCache &info);
+
+struct OnlineBackupVersion
+{
+    //! the version of the online backup currently enabled
+    std::string version;
+    //! the algorithm used by the backup
+    std::string algorithm;
+};
+
+void
+to_json(nlohmann::json &j, const OnlineBackupVersion &info);
+void
+from_json(const nlohmann::json &j, OnlineBackupVersion &info);
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index 4a5c5c7613a34eeaf3cc11a1f84e9ca8bf4f116e..e28f5b2dddd2e44e800e8498c8926c5b1307af56 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -16,23 +16,23 @@
 namespace cache {
 enum class CacheVersion : int
 {
-        Older   = -1,
-        Current = 0,
-        Newer   = 1,
+    Older   = -1,
+    Current = 0,
+    Newer   = 1,
 };
 }
 
 struct RoomMember
 {
-        QString user_id;
-        QString display_name;
+    QString user_id;
+    QString display_name;
 };
 
 //! Used to uniquely identify a list of read receipts.
 struct ReadReceiptKey
 {
-        std::string event_id;
-        std::string room_id;
+    std::string event_id;
+    std::string room_id;
 };
 
 void
@@ -43,49 +43,49 @@ from_json(const nlohmann::json &j, ReadReceiptKey &key);
 
 struct DescInfo
 {
-        QString event_id;
-        QString userid;
-        QString body;
-        QString descriptiveTime;
-        uint64_t timestamp;
-        QDateTime datetime;
+    QString event_id;
+    QString userid;
+    QString body;
+    QString descriptiveTime;
+    uint64_t timestamp;
+    QDateTime datetime;
 };
 
 inline bool
 operator==(const DescInfo &a, const DescInfo &b)
 {
-        return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) ==
-               std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
+    return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) ==
+           std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
 }
 inline bool
 operator!=(const DescInfo &a, const DescInfo &b)
 {
-        return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) !=
-               std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
+    return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) !=
+           std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
 }
 
 //! UI info associated with a room.
 struct RoomInfo
 {
-        //! The calculated name of the room.
-        std::string name;
-        //! The topic of the room.
-        std::string topic;
-        //! The calculated avatar url of the room.
-        std::string avatar_url;
-        //! The calculated version of this room set at creation time.
-        std::string version;
-        //! Whether or not the room is an invite.
-        bool is_invite = false;
-        //! Wheter or not the room is a space
-        bool is_space = false;
-        //! Total number of members in the room.
-        size_t member_count = 0;
-        //! Who can access to the room.
-        mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public;
-        bool guest_access                      = false;
-        //! The list of tags associated with this room
-        std::vector<std::string> tags;
+    //! The calculated name of the room.
+    std::string name;
+    //! The topic of the room.
+    std::string topic;
+    //! The calculated avatar url of the room.
+    std::string avatar_url;
+    //! The calculated version of this room set at creation time.
+    std::string version;
+    //! Whether or not the room is an invite.
+    bool is_invite = false;
+    //! Wheter or not the room is a space
+    bool is_space = false;
+    //! Total number of members in the room.
+    size_t member_count = 0;
+    //! Who can access to the room.
+    mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public;
+    bool guest_access                      = false;
+    //! The list of tags associated with this room
+    std::vector<std::string> tags;
 };
 
 void
@@ -93,11 +93,11 @@ to_json(nlohmann::json &j, const RoomInfo &info);
 void
 from_json(const nlohmann::json &j, RoomInfo &info);
 
-//! Basic information per member;
+//! Basic information per member.
 struct MemberInfo
 {
-        std::string name;
-        std::string avatar_url;
+    std::string name;
+    std::string avatar_url;
 };
 
 void
@@ -107,13 +107,13 @@ from_json(const nlohmann::json &j, MemberInfo &info);
 
 struct RoomSearchResult
 {
-        std::string room_id;
-        RoomInfo info;
+    std::string room_id;
+    RoomInfo info;
 };
 
 struct ImagePackInfo
 {
-        mtx::events::msc2545::ImagePack pack;
-        std::string source_room;
-        std::string state_key;
+    mtx::events::msc2545::ImagePack pack;
+    std::string source_room;
+    std::string state_key;
 };
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 748404d161fd7ca4c062454843c3d4af365e0aeb..651d73d7f358b53c52ad8712b4b46f54a51f4910 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -32,687 +32,660 @@
 
 class Cache : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        Cache(const QString &userId, QObject *parent = nullptr);
-
-        std::string displayName(const std::string &room_id, const std::string &user_id);
-        QString displayName(const QString &room_id, const QString &user_id);
-        QString avatarUrl(const QString &room_id, const QString &user_id);
-
-        // presence
-        mtx::presence::PresenceState presenceState(const std::string &user_id);
-        std::string statusMessage(const std::string &user_id);
-
-        // user cache stores user keys
-        std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
-          const std::string &room_id,
-          bool verified_only);
-        void updateUserKeys(const std::string &sync_token,
-                            const mtx::responses::QueryKeys &keyQuery);
-        void markUserKeysOutOfDate(lmdb::txn &txn,
-                                   lmdb::dbi &db,
-                                   const std::vector<std::string> &user_ids,
-                                   const std::string &sync_token);
-        void deleteUserKeys(lmdb::txn &txn,
-                            lmdb::dbi &db,
-                            const std::vector<std::string> &user_ids);
-        void query_keys(const std::string &user_id,
-                        std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
-
-        // device & user verification cache
-        std::optional<UserKeyCache> userKeys(const std::string &user_id);
-        VerificationStatus verificationStatus(const std::string &user_id);
-        void markDeviceVerified(const std::string &user_id, const std::string &device);
-        void markDeviceUnverified(const std::string &user_id, const std::string &device);
-        crypto::Trust roomVerificationStatus(const std::string &room_id);
-
-        std::vector<std::string> joinedRooms();
-
-        QMap<QString, RoomInfo> roomInfo(bool withInvites = true);
-        std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid);
-        QHash<QString, RoomInfo> invites();
-        std::optional<RoomInfo> invite(std::string_view roomid);
-        QMap<QString, std::optional<RoomInfo>> spaces();
-
-        //! Calculate & return the name of the room.
-        QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        //! Get room join rules
-        mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb);
-        bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb);
-        //! Retrieve the topic of the room if any.
-        QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
-        //! Retrieve the room avatar's url if any.
-        QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        //! Retrieve the version of the room if any.
-        QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
-        //! Retrieve if the room is a space
-        bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
-
-        //! Get a specific state event
-        template<typename T>
-        std::optional<mtx::events::StateEvent<T>> getStateEvent(const std::string &room_id,
-                                                                std::string_view state_key = "")
-        {
-                auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
-                return getStateEvent<T>(txn, room_id, state_key);
-        }
-
-        //! retrieve a specific event from account data
-        //! pass empty room_id for global account data
-        std::optional<mtx::events::collections::RoomAccountDataEvents> getAccountData(
-          mtx::events::EventType type,
-          const std::string &room_id = "");
-
-        //! Retrieve member info from a room.
-        std::vector<RoomMember> getMembers(const std::string &room_id,
-                                           std::size_t startIndex = 0,
-                                           std::size_t len        = 30);
-        size_t memberCount(const std::string &room_id);
-
-        void saveState(const mtx::responses::Sync &res);
-        bool isInitialized();
-        bool isDatabaseReady() { return databaseReady_ && isInitialized(); }
-
-        std::string nextBatchToken();
-
-        void deleteData();
-
-        void removeInvite(lmdb::txn &txn, const std::string &room_id);
-        void removeInvite(const std::string &room_id);
-        void removeRoom(lmdb::txn &txn, const std::string &roomid);
-        void removeRoom(const std::string &roomid);
-        void setup();
-
-        cache::CacheVersion formatVersion();
-        void setCurrentFormat();
-        bool runMigrations();
-
-        std::vector<QString> roomIds();
-        QMap<QString, mtx::responses::Notifications> getTimelineMentions();
-
-        //! Retrieve all the user ids from a room.
-        std::vector<std::string> roomMembers(const std::string &room_id);
-
-        //! Check if the given user has power leve greater than than
-        //! lowest power level of the given events.
-        bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
+    Cache(const QString &userId, QObject *parent = nullptr);
+
+    std::string displayName(const std::string &room_id, const std::string &user_id);
+    QString displayName(const QString &room_id, const QString &user_id);
+    QString avatarUrl(const QString &room_id, const QString &user_id);
+
+    // presence
+    mtx::presence::PresenceState presenceState(const std::string &user_id);
+    std::string statusMessage(const std::string &user_id);
+
+    // user cache stores user keys
+    std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
+      const std::string &room_id,
+      bool verified_only);
+    void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
+    void markUserKeysOutOfDate(const std::vector<std::string> &user_ids);
+    void markUserKeysOutOfDate(lmdb::txn &txn,
+                               lmdb::dbi &db,
+                               const std::vector<std::string> &user_ids,
+                               const std::string &sync_token);
+    void query_keys(const std::string &user_id,
+                    std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
+
+    // device & user verification cache
+    std::optional<UserKeyCache> userKeys(const std::string &user_id);
+    VerificationStatus verificationStatus(const std::string &user_id);
+    void markDeviceVerified(const std::string &user_id, const std::string &device);
+    void markDeviceUnverified(const std::string &user_id, const std::string &device);
+    crypto::Trust roomVerificationStatus(const std::string &room_id);
+
+    std::vector<std::string> joinedRooms();
+
+    QMap<QString, RoomInfo> roomInfo(bool withInvites = true);
+    std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid);
+    QHash<QString, RoomInfo> invites();
+    std::optional<RoomInfo> invite(std::string_view roomid);
+    QMap<QString, std::optional<RoomInfo>> spaces();
+
+    //! Calculate & return the name of the room.
+    QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    //! Get room join rules
+    mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb);
+    bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb);
+    //! Retrieve the topic of the room if any.
+    QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
+    //! Retrieve the room avatar's url if any.
+    QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    //! Retrieve the version of the room if any.
+    QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
+    //! Retrieve if the room is a space
+    bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
+
+    //! Get a specific state event
+    template<typename T>
+    std::optional<mtx::events::StateEvent<T>> getStateEvent(const std::string &room_id,
+                                                            std::string_view state_key = "")
+    {
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        return getStateEvent<T>(txn, room_id, state_key);
+    }
+
+    //! retrieve a specific event from account data
+    //! pass empty room_id for global account data
+    std::optional<mtx::events::collections::RoomAccountDataEvents> getAccountData(
+      mtx::events::EventType type,
+      const std::string &room_id = "");
+
+    //! Retrieve member info from a room.
+    std::vector<RoomMember> getMembers(const std::string &room_id,
+                                       std::size_t startIndex = 0,
+                                       std::size_t len        = 30);
+
+    std::vector<RoomMember> getMembersFromInvite(const std::string &room_id,
+                                                 std::size_t startIndex = 0,
+                                                 std::size_t len        = 30);
+    size_t memberCount(const std::string &room_id);
+
+    void saveState(const mtx::responses::Sync &res);
+    bool isInitialized();
+    bool isDatabaseReady() { return databaseReady_ && isInitialized(); }
+
+    std::string nextBatchToken();
+
+    void deleteData();
+
+    void removeInvite(lmdb::txn &txn, const std::string &room_id);
+    void removeInvite(const std::string &room_id);
+    void removeRoom(lmdb::txn &txn, const std::string &roomid);
+    void removeRoom(const std::string &roomid);
+    void setup();
+
+    cache::CacheVersion formatVersion();
+    void setCurrentFormat();
+    bool runMigrations();
+
+    std::vector<QString> roomIds();
+    QMap<QString, mtx::responses::Notifications> getTimelineMentions();
+
+    //! Retrieve all the user ids from a room.
+    std::vector<std::string> roomMembers(const std::string &room_id);
+
+    //! Check if the given user has power leve greater than than
+    //! lowest power level of the given events.
+    bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
+                             const std::string &room_id,
+                             const std::string &user_id);
+
+    //! Adds a user to the read list for the given event.
+    //!
+    //! There should be only one user id present in a receipt list per room.
+    //! The user id should be removed from any other lists.
+    using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
+    void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts);
+
+    //! Retrieve all the read receipts for the given event id and room.
+    //!
+    //! Returns a map of user ids and the time of the read receipt in milliseconds.
+    using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
+    UserReceipts readReceipts(const QString &event_id, const QString &room_id);
+
+    RoomInfo singleRoomInfo(const std::string &room_id);
+    std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
+    std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms);
+
+    //! Calculates which the read status of a room.
+    //! Whether all the events in the timeline have been read.
+    bool calculateRoomReadStatus(const std::string &room_id);
+    void calculateRoomReadStatus();
+
+    void markSentNotification(const std::string &event_id);
+    //! Removes an event from the sent notifications.
+    void removeReadNotification(const std::string &event_id);
+    //! Check if we have sent a desktop notification for the given event id.
+    bool isNotificationSent(const std::string &event_id);
+
+    //! Add all notifications containing a user mention to the db.
+    void saveTimelineMentions(const mtx::responses::Notifications &res);
+
+    //! retrieve events in timeline and related functions
+    struct Messages
+    {
+        mtx::responses::Timeline timeline;
+        uint64_t next_index;
+        bool end_of_cache = false;
+    };
+    Messages getTimelineMessages(lmdb::txn &txn,
                                  const std::string &room_id,
-                                 const std::string &user_id);
-
-        //! Adds a user to the read list for the given event.
-        //!
-        //! There should be only one user id present in a receipt list per room.
-        //! The user id should be removed from any other lists.
-        using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
-        void updateReadReceipt(lmdb::txn &txn,
-                               const std::string &room_id,
-                               const Receipts &receipts);
-
-        //! Retrieve all the read receipts for the given event id and room.
-        //!
-        //! Returns a map of user ids and the time of the read receipt in milliseconds.
-        using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
-        UserReceipts readReceipts(const QString &event_id, const QString &room_id);
-
-        RoomInfo singleRoomInfo(const std::string &room_id);
-        std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
-        std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms);
-
-        //! Calculates which the read status of a room.
-        //! Whether all the events in the timeline have been read.
-        bool calculateRoomReadStatus(const std::string &room_id);
-        void calculateRoomReadStatus();
-
-        void markSentNotification(const std::string &event_id);
-        //! Removes an event from the sent notifications.
-        void removeReadNotification(const std::string &event_id);
-        //! Check if we have sent a desktop notification for the given event id.
-        bool isNotificationSent(const std::string &event_id);
-
-        //! Add all notifications containing a user mention to the db.
-        void saveTimelineMentions(const mtx::responses::Notifications &res);
-
-        //! retrieve events in timeline and related functions
-        struct Messages
-        {
-                mtx::responses::Timeline timeline;
-                uint64_t next_index;
-                bool end_of_cache = false;
-        };
-        Messages getTimelineMessages(lmdb::txn &txn,
-                                     const std::string &room_id,
-                                     uint64_t index = std::numeric_limits<uint64_t>::max(),
-                                     bool forward   = false);
-
-        std::optional<mtx::events::collections::TimelineEvent> getEvent(
-          const std::string &room_id,
-          const std::string &event_id);
-        void storeEvent(const std::string &room_id,
-                        const std::string &event_id,
-                        const mtx::events::collections::TimelineEvent &event);
-        void replaceEvent(const std::string &room_id,
-                          const std::string &event_id,
-                          const mtx::events::collections::TimelineEvent &event);
-        std::vector<std::string> relatedEvents(const std::string &room_id,
-                                               const std::string &event_id);
-
-        struct TimelineRange
-        {
-                uint64_t first, last;
+                                 uint64_t index = std::numeric_limits<uint64_t>::max(),
+                                 bool forward   = false);
+
+    std::optional<mtx::events::collections::TimelineEvent> getEvent(const std::string &room_id,
+                                                                    const std::string &event_id);
+    void storeEvent(const std::string &room_id,
+                    const std::string &event_id,
+                    const mtx::events::collections::TimelineEvent &event);
+    void replaceEvent(const std::string &room_id,
+                      const std::string &event_id,
+                      const mtx::events::collections::TimelineEvent &event);
+    std::vector<std::string> relatedEvents(const std::string &room_id, const std::string &event_id);
+
+    struct TimelineRange
+    {
+        uint64_t first, last;
+    };
+    std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
+    std::optional<uint64_t> getTimelineIndex(const std::string &room_id, std::string_view event_id);
+    std::optional<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id);
+    std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter(
+      const std::string &room_id,
+      std::string_view event_id);
+    std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
+    std::optional<uint64_t> getArrivalIndex(const std::string &room_id, std::string_view event_id);
+
+    std::string previousBatchToken(const std::string &room_id);
+    uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
+    void savePendingMessage(const std::string &room_id,
+                            const mtx::events::collections::TimelineEvent &message);
+    std::optional<mtx::events::collections::TimelineEvent> firstPendingMessage(
+      const std::string &room_id);
+    void removePendingStatus(const std::string &room_id, const std::string &txn_id);
+
+    //! clear timeline keeping only the latest batch
+    void clearTimeline(const std::string &room_id);
+
+    //! Remove old unused data.
+    void deleteOldMessages();
+    void deleteOldData() noexcept;
+    //! Retrieve all saved room ids.
+    std::vector<std::string> getRoomIds(lmdb::txn &txn);
+    std::vector<std::string> getParentRoomIds(const std::string &room_id);
+    std::vector<std::string> getChildRoomIds(const std::string &room_id);
+
+    std::vector<ImagePackInfo> getImagePacks(const std::string &room_id,
+                                             std::optional<bool> stickers);
+
+    //! Mark a room that uses e2e encryption.
+    void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
+    bool isRoomEncrypted(const std::string &room_id);
+    std::optional<mtx::events::state::Encryption> roomEncryptionSettings(
+      const std::string &room_id);
+
+    //! Check if a user is a member of the room.
+    bool isRoomMember(const std::string &user_id, const std::string &room_id);
+
+    //
+    // Outbound Megolm Sessions
+    //
+    void saveOutboundMegolmSession(const std::string &room_id,
+                                   const GroupSessionData &data,
+                                   mtx::crypto::OutboundGroupSessionPtr &session);
+    OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
+    bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
+    void updateOutboundMegolmSession(const std::string &room_id,
+                                     const GroupSessionData &data,
+                                     mtx::crypto::OutboundGroupSessionPtr &session);
+    void dropOutboundMegolmSession(const std::string &room_id);
+
+    void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
+    mtx::crypto::ExportedSessionKeys exportSessionKeys();
+
+    //
+    // Inbound Megolm Sessions
+    //
+    void saveInboundMegolmSession(const MegolmSessionIndex &index,
+                                  mtx::crypto::InboundGroupSessionPtr session,
+                                  const GroupSessionData &data);
+    mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index);
+    bool inboundMegolmSessionExists(const MegolmSessionIndex &index);
+    std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index);
+
+    //
+    // Olm Sessions
+    //
+    void saveOlmSession(const std::string &curve25519,
+                        mtx::crypto::OlmSessionPtr session,
+                        uint64_t timestamp);
+    std::vector<std::string> getOlmSessions(const std::string &curve25519);
+    std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519,
+                                                            const std::string &session_id);
+    std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(const std::string &curve25519);
+
+    void saveOlmAccount(const std::string &pickled);
+    std::string restoreOlmAccount();
+
+    void saveBackupVersion(const OnlineBackupVersion &data);
+    void deleteBackupVersion();
+    std::optional<OnlineBackupVersion> backupVersion();
+
+    void storeSecret(const std::string name, const std::string secret, bool internal = false);
+    void deleteSecret(const std::string name, bool internal = false);
+    std::optional<std::string> secret(const std::string name, bool internal = false);
+
+    std::string pickleSecret();
+
+    template<class T>
+    constexpr static bool isStateEvent_ =
+      std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
+                     mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
+
+    static int compare_state_key(const MDB_val *a, const MDB_val *b)
+    {
+        auto get_skey = [](const MDB_val *v) {
+            return nlohmann::json::parse(
+                     std::string_view(static_cast<const char *>(v->mv_data), v->mv_size))
+              .value("key", "");
         };
-        std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
-        std::optional<uint64_t> getTimelineIndex(const std::string &room_id,
-                                                 std::string_view event_id);
-        std::optional<uint64_t> getEventIndex(const std::string &room_id,
-                                              std::string_view event_id);
-        std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter(
-          const std::string &room_id,
-          std::string_view event_id);
-        std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
-        std::optional<uint64_t> getArrivalIndex(const std::string &room_id,
-                                                std::string_view event_id);
-
-        std::string previousBatchToken(const std::string &room_id);
-        uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
-        void savePendingMessage(const std::string &room_id,
-                                const mtx::events::collections::TimelineEvent &message);
-        std::optional<mtx::events::collections::TimelineEvent> firstPendingMessage(
-          const std::string &room_id);
-        void removePendingStatus(const std::string &room_id, const std::string &txn_id);
-
-        //! clear timeline keeping only the latest batch
-        void clearTimeline(const std::string &room_id);
-
-        //! Remove old unused data.
-        void deleteOldMessages();
-        void deleteOldData() noexcept;
-        //! Retrieve all saved room ids.
-        std::vector<std::string> getRoomIds(lmdb::txn &txn);
-        std::vector<std::string> getParentRoomIds(const std::string &room_id);
-        std::vector<std::string> getChildRoomIds(const std::string &room_id);
-
-        std::vector<ImagePackInfo> getImagePacks(const std::string &room_id,
-                                                 std::optional<bool> stickers);
-
-        //! Mark a room that uses e2e encryption.
-        void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
-        bool isRoomEncrypted(const std::string &room_id);
-        std::optional<mtx::events::state::Encryption> roomEncryptionSettings(
-          const std::string &room_id);
-
-        //! Check if a user is a member of the room.
-        bool isRoomMember(const std::string &user_id, const std::string &room_id);
-
-        //
-        // Outbound Megolm Sessions
-        //
-        void saveOutboundMegolmSession(const std::string &room_id,
-                                       const GroupSessionData &data,
-                                       mtx::crypto::OutboundGroupSessionPtr &session);
-        OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
-        bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
-        void updateOutboundMegolmSession(const std::string &room_id,
-                                         const GroupSessionData &data,
-                                         mtx::crypto::OutboundGroupSessionPtr &session);
-        void dropOutboundMegolmSession(const std::string &room_id);
-
-        void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
-        mtx::crypto::ExportedSessionKeys exportSessionKeys();
-
-        //
-        // Inbound Megolm Sessions
-        //
-        void saveInboundMegolmSession(const MegolmSessionIndex &index,
-                                      mtx::crypto::InboundGroupSessionPtr session,
-                                      const GroupSessionData &data);
-        mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(
-          const MegolmSessionIndex &index);
-        bool inboundMegolmSessionExists(const MegolmSessionIndex &index);
-        std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index);
-
-        //
-        // Olm Sessions
-        //
-        void saveOlmSession(const std::string &curve25519,
-                            mtx::crypto::OlmSessionPtr session,
-                            uint64_t timestamp);
-        std::vector<std::string> getOlmSessions(const std::string &curve25519);
-        std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519,
-                                                                const std::string &session_id);
-        std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(
-          const std::string &curve25519);
-
-        void saveOlmAccount(const std::string &pickled);
-        std::string restoreOlmAccount();
-
-        void storeSecret(const std::string name, const std::string secret);
-        void deleteSecret(const std::string name);
-        std::optional<std::string> secret(const std::string name);
-
-        template<class T>
-        constexpr static bool isStateEvent_ =
-          std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
-                         mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
-
-        static int compare_state_key(const MDB_val *a, const MDB_val *b)
-        {
-                auto get_skey = [](const MDB_val *v) {
-                        return nlohmann::json::parse(
-                                 std::string_view(static_cast<const char *>(v->mv_data),
-                                                  v->mv_size))
-                          .value("key", "");
-                };
-
-                return get_skey(a).compare(get_skey(b));
-        }
 
+        return get_skey(a).compare(get_skey(b));
+    }
 signals:
-        void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
-        void roomReadStatus(const std::map<QString, bool> &status);
-        void removeNotification(const QString &room_id, const QString &event_id);
-        void userKeysUpdate(const std::string &sync_token,
-                            const mtx::responses::QueryKeys &keyQuery);
-        void verificationStatusChanged(const std::string &userid);
-        void secretChanged(const std::string name);
+    void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
+    void roomReadStatus(const std::map<QString, bool> &status);
+    void removeNotification(const QString &room_id, const QString &event_id);
+    void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
+    void verificationStatusChanged(const std::string &userid);
+    void selfVerificationStatusChanged();
+    void secretChanged(const std::string name);
 
 private:
-        //! Save an invited room.
-        void saveInvite(lmdb::txn &txn,
+    //! Save an invited room.
+    void saveInvite(lmdb::txn &txn,
+                    lmdb::dbi &statesdb,
+                    lmdb::dbi &membersdb,
+                    const mtx::responses::InvitedRoom &room);
+
+    //! Add a notification containing a user mention to the db.
+    void saveTimelineMentions(lmdb::txn &txn,
+                              const std::string &room_id,
+                              const QList<mtx::responses::Notification> &res);
+
+    //! Get timeline items that a user was mentions in for a given room
+    mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn,
+                                                             const std::string &room_id);
+
+    QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
+    QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db);
+
+    std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id);
+
+    std::string getLastEventId(lmdb::txn &txn, const std::string &room_id);
+    void saveTimelineMessages(lmdb::txn &txn,
+                              lmdb::dbi &eventsDb,
+                              const std::string &room_id,
+                              const mtx::responses::Timeline &res);
+
+    //! retrieve a specific event from account data
+    //! pass empty room_id for global account data
+    std::optional<mtx::events::collections::RoomAccountDataEvents>
+    getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
+    bool isHiddenEvent(lmdb::txn &txn,
+                       mtx::events::collections::TimelineEvents e,
+                       const std::string &room_id);
+
+    //! Remove a room from the cache.
+    // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
+    template<class T>
+    void saveStateEvents(lmdb::txn &txn,
+                         lmdb::dbi &statesdb,
+                         lmdb::dbi &stateskeydb,
+                         lmdb::dbi &membersdb,
+                         lmdb::dbi &eventsDb,
+                         const std::string &room_id,
+                         const std::vector<T> &events)
+    {
+        for (const auto &e : events)
+            saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e);
+    }
+
+    template<class T>
+    void saveStateEvent(lmdb::txn &txn,
                         lmdb::dbi &statesdb,
+                        lmdb::dbi &stateskeydb,
                         lmdb::dbi &membersdb,
-                        const mtx::responses::InvitedRoom &room);
-
-        //! Add a notification containing a user mention to the db.
-        void saveTimelineMentions(lmdb::txn &txn,
-                                  const std::string &room_id,
-                                  const QList<mtx::responses::Notification> &res);
-
-        //! Get timeline items that a user was mentions in for a given room
-        mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn,
-                                                                 const std::string &room_id);
-
-        QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
-        QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db);
-
-        std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id);
-
-        std::string getLastEventId(lmdb::txn &txn, const std::string &room_id);
-        void saveTimelineMessages(lmdb::txn &txn,
-                                  lmdb::dbi &eventsDb,
-                                  const std::string &room_id,
-                                  const mtx::responses::Timeline &res);
-
-        //! retrieve a specific event from account data
-        //! pass empty room_id for global account data
-        std::optional<mtx::events::collections::RoomAccountDataEvents>
-        getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
-        bool isHiddenEvent(lmdb::txn &txn,
-                           mtx::events::collections::TimelineEvents e,
-                           const std::string &room_id);
-
-        //! Remove a room from the cache.
-        // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
-        template<class T>
-        void saveStateEvents(lmdb::txn &txn,
-                             lmdb::dbi &statesdb,
-                             lmdb::dbi &stateskeydb,
-                             lmdb::dbi &membersdb,
-                             lmdb::dbi &eventsDb,
-                             const std::string &room_id,
-                             const std::vector<T> &events)
-        {
-                for (const auto &e : events)
-                        saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e);
+                        lmdb::dbi &eventsDb,
+                        const std::string &room_id,
+                        const T &event)
+    {
+        using namespace mtx::events;
+        using namespace mtx::events::state;
+
+        if (auto e = std::get_if<StateEvent<Member>>(&event); e != nullptr) {
+            switch (e->content.membership) {
+            //
+            // We only keep users with invite or join membership.
+            //
+            case Membership::Invite:
+            case Membership::Join: {
+                auto display_name =
+                  e->content.display_name.empty() ? e->state_key : e->content.display_name;
+
+                // Lightweight representation of a member.
+                MemberInfo tmp{display_name, e->content.avatar_url};
+
+                membersdb.put(txn, e->state_key, json(tmp).dump());
+                break;
+            }
+            default: {
+                membersdb.del(txn, e->state_key, "");
+                break;
+            }
+            }
+
+            return;
+        } else if (std::holds_alternative<StateEvent<Encryption>>(event)) {
+            setEncryptedRoom(txn, room_id);
+            return;
         }
 
-        template<class T>
-        void saveStateEvent(lmdb::txn &txn,
-                            lmdb::dbi &statesdb,
-                            lmdb::dbi &stateskeydb,
-                            lmdb::dbi &membersdb,
-                            lmdb::dbi &eventsDb,
-                            const std::string &room_id,
-                            const T &event)
-        {
-                using namespace mtx::events;
-                using namespace mtx::events::state;
-
-                if (auto e = std::get_if<StateEvent<Member>>(&event); e != nullptr) {
-                        switch (e->content.membership) {
-                        //
-                        // We only keep users with invite or join membership.
-                        //
-                        case Membership::Invite:
-                        case Membership::Join: {
-                                auto display_name = e->content.display_name.empty()
-                                                      ? e->state_key
-                                                      : e->content.display_name;
-
-                                // Lightweight representation of a member.
-                                MemberInfo tmp{display_name, e->content.avatar_url};
-
-                                membersdb.put(txn, e->state_key, json(tmp).dump());
-                                break;
-                        }
-                        default: {
-                                membersdb.del(txn, e->state_key, "");
-                                break;
-                        }
-                        }
-
-                        return;
-                } else if (std::holds_alternative<StateEvent<Encryption>>(event)) {
-                        setEncryptedRoom(txn, room_id);
-                        return;
-                }
-
-                std::visit(
-                  [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
-                          if constexpr (isStateEvent_<decltype(e)>) {
-                                  eventsDb.put(txn, e.event_id, json(e).dump());
-
-                                  if (e.type != EventType::Unsupported) {
-                                          if (std::is_same_v<
-                                                std::remove_cv_t<
-                                                  std::remove_reference_t<decltype(e)>>,
-                                                StateEvent<mtx::events::msg::Redacted>>) {
-                                                  if (e.type == EventType::RoomMember)
-                                                          membersdb.del(txn, e.state_key, "");
-                                                  else if (e.state_key.empty())
-                                                          statesdb.del(txn, to_string(e.type));
-                                                  else
-                                                          stateskeydb.del(
-                                                            txn,
-                                                            to_string(e.type),
-                                                            json::object({
-                                                                           {"key", e.state_key},
-                                                                           {"id", e.event_id},
-                                                                         })
-                                                              .dump());
-                                          } else if (e.state_key.empty())
-                                                  statesdb.put(
-                                                    txn, to_string(e.type), json(e).dump());
-                                          else
-                                                  stateskeydb.put(
-                                                    txn,
-                                                    to_string(e.type),
-                                                    json::object({
-                                                                   {"key", e.state_key},
-                                                                   {"id", e.event_id},
-                                                                 })
-                                                      .dump());
-                                  }
-                          }
-                  },
-                  event);
+        std::visit(
+          [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
+              if constexpr (isStateEvent_<decltype(e)>) {
+                  eventsDb.put(txn, e.event_id, json(e).dump());
+
+                  if (e.type != EventType::Unsupported) {
+                      if (std::is_same_v<std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
+                                         StateEvent<mtx::events::msg::Redacted>>) {
+                          if (e.type == EventType::RoomMember)
+                              membersdb.del(txn, e.state_key, "");
+                          else if (e.state_key.empty())
+                              statesdb.del(txn, to_string(e.type));
+                          else
+                              stateskeydb.del(txn,
+                                              to_string(e.type),
+                                              json::object({
+                                                             {"key", e.state_key},
+                                                             {"id", e.event_id},
+                                                           })
+                                                .dump());
+                      } else if (e.state_key.empty())
+                          statesdb.put(txn, to_string(e.type), json(e).dump());
+                      else
+                          stateskeydb.put(txn,
+                                          to_string(e.type),
+                                          json::object({
+                                                         {"key", e.state_key},
+                                                         {"id", e.event_id},
+                                                       })
+                                            .dump());
+                  }
+              }
+          },
+          event);
+    }
+
+    template<typename T>
+    std::optional<mtx::events::StateEvent<T>> getStateEvent(lmdb::txn &txn,
+                                                            const std::string &room_id,
+                                                            std::string_view state_key = "")
+    {
+        constexpr auto type = mtx::events::state_content_to_type<T>;
+        static_assert(type != mtx::events::EventType::Unsupported,
+                      "Not a supported type in state events.");
+
+        if (room_id.empty())
+            return std::nullopt;
+        const auto typeStr = to_string(type);
+
+        std::string_view value;
+        if (state_key.empty()) {
+            auto db = getStatesDb(txn, room_id);
+            if (!db.get(txn, typeStr, value)) {
+                return std::nullopt;
+            }
+        } else {
+            auto db                   = getStatesKeyDb(txn, room_id);
+            std::string d             = json::object({{"key", state_key}}).dump();
+            std::string_view data     = d;
+            std::string_view typeStrV = typeStr;
+
+            auto cursor = lmdb::cursor::open(txn, db);
+            if (!cursor.get(typeStrV, data, MDB_GET_BOTH))
+                return std::nullopt;
+
+            try {
+                auto eventsDb = getEventsDb(txn, room_id);
+                if (!eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value))
+                    return std::nullopt;
+            } catch (std::exception &e) {
+                return std::nullopt;
+            }
         }
 
-        template<typename T>
-        std::optional<mtx::events::StateEvent<T>> getStateEvent(lmdb::txn &txn,
-                                                                const std::string &room_id,
-                                                                std::string_view state_key = "")
-        {
-                constexpr auto type = mtx::events::state_content_to_type<T>;
-                static_assert(type != mtx::events::EventType::Unsupported,
-                              "Not a supported type in state events.");
-
-                if (room_id.empty())
-                        return std::nullopt;
-                const auto typeStr = to_string(type);
-
-                std::string_view value;
-                if (state_key.empty()) {
-                        auto db = getStatesDb(txn, room_id);
-                        if (!db.get(txn, typeStr, value)) {
-                                return std::nullopt;
-                        }
-                } else {
-                        auto db                   = getStatesKeyDb(txn, room_id);
-                        std::string d             = json::object({{"key", state_key}}).dump();
-                        std::string_view data     = d;
-                        std::string_view typeStrV = typeStr;
-
-                        auto cursor = lmdb::cursor::open(txn, db);
-                        if (!cursor.get(typeStrV, data, MDB_GET_BOTH))
-                                return std::nullopt;
-
-                        try {
-                                auto eventsDb = getEventsDb(txn, room_id);
-                                if (!eventsDb.get(
-                                      txn, json::parse(data)["id"].get<std::string>(), value))
-                                        return std::nullopt;
-                        } catch (std::exception &e) {
-                                return std::nullopt;
-                        }
-                }
-
-                try {
-                        return json::parse(value).get<mtx::events::StateEvent<T>>();
-                } catch (std::exception &e) {
-                        return std::nullopt;
-                }
+        try {
+            return json::parse(value).get<mtx::events::StateEvent<T>>();
+        } catch (std::exception &e) {
+            return std::nullopt;
         }
+    }
 
-        template<typename T>
-        std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(lmdb::txn &txn,
-                                                                       const std::string &room_id)
+    template<typename T>
+    std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(lmdb::txn &txn,
+                                                                   const std::string &room_id)
 
-        {
-                constexpr auto type = mtx::events::state_content_to_type<T>;
-                static_assert(type != mtx::events::EventType::Unsupported,
-                              "Not a supported type in state events.");
-
-                if (room_id.empty())
-                        return {};
-
-                std::vector<mtx::events::StateEvent<T>> events;
-
-                {
-                        auto db                   = getStatesKeyDb(txn, room_id);
-                        auto eventsDb             = getEventsDb(txn, room_id);
-                        const auto typeStr        = to_string(type);
-                        std::string_view typeStrV = typeStr;
-                        std::string_view data;
-                        std::string_view value;
-
-                        auto cursor = lmdb::cursor::open(txn, db);
-                        bool first  = true;
-                        if (cursor.get(typeStrV, data, MDB_SET)) {
-                                while (cursor.get(
-                                  typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                        first = false;
-
-                                        if (eventsDb.get(txn,
-                                                         json::parse(data)["id"].get<std::string>(),
-                                                         value))
-                                                events.push_back(
-                                                  json::parse(value)
-                                                    .get<mtx::events::StateEvent<T>>());
-                                }
-                        }
-                }
+    {
+        constexpr auto type = mtx::events::state_content_to_type<T>;
+        static_assert(type != mtx::events::EventType::Unsupported,
+                      "Not a supported type in state events.");
 
-                return events;
-        }
-        void saveInvites(lmdb::txn &txn,
-                         const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
+        if (room_id.empty())
+            return {};
 
-        void savePresence(
-          lmdb::txn &txn,
-          const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates);
+        std::vector<mtx::events::StateEvent<T>> events;
 
-        //! Sends signals for the rooms that are removed.
-        void removeLeftRooms(lmdb::txn &txn,
-                             const std::map<std::string, mtx::responses::LeftRoom> &rooms)
         {
-                for (const auto &room : rooms) {
-                        removeRoom(txn, room.first);
-
-                        // Clean up leftover invites.
-                        removeInvite(txn, room.first);
+            auto db                   = getStatesKeyDb(txn, room_id);
+            auto eventsDb             = getEventsDb(txn, room_id);
+            const auto typeStr        = to_string(type);
+            std::string_view typeStrV = typeStr;
+            std::string_view data;
+            std::string_view value;
+
+            auto cursor = lmdb::cursor::open(txn, db);
+            bool first  = true;
+            if (cursor.get(typeStrV, data, MDB_SET)) {
+                while (cursor.get(typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                    first = false;
+
+                    if (eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value))
+                        events.push_back(json::parse(value).get<mtx::events::StateEvent<T>>());
                 }
+            }
         }
 
-        void updateSpaces(lmdb::txn &txn,
-                          const std::set<std::string> &spaces_with_updates,
-                          std::set<std::string> rooms_with_updates);
-
-        lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
-        }
-
-        lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
-        }
-
-        lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
-        }
-
-        // inverse of EventOrderDb
-        lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
-        }
+        return events;
+    }
+    void saveInvites(lmdb::txn &txn,
+                     const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
 
-        lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
-        }
+    void savePresence(
+      lmdb::txn &txn,
+      const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates);
 
-        lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
-        }
+    //! Sends signals for the rooms that are removed.
+    void removeLeftRooms(lmdb::txn &txn,
+                         const std::map<std::string, mtx::responses::LeftRoom> &rooms)
+    {
+        for (const auto &room : rooms) {
+            removeRoom(txn, room.first);
 
-        lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+            // Clean up leftover invites.
+            removeInvite(txn, room.first);
         }
-
-        lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
-        }
-
-        lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
-        }
-
-        lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
-        }
-
-        lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
-        }
-
-        lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                auto db = lmdb::dbi::open(
-                  txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT);
-                lmdb::dbi_set_dupsort(txn, db, compare_state_key);
-                return db;
-        }
-
-        lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
-        }
-
-        lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
-        }
-
-        lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE);
-        }
-
-        lmdb::dbi getPresenceDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "presence", MDB_CREATE);
-        }
-
-        lmdb::dbi getUserKeysDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
-        }
-
-        lmdb::dbi getVerificationDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "verified", MDB_CREATE);
-        }
-
-        //! Retrieves or creates the database that stores the open OLM sessions between our device
-        //! and the given curve25519 key which represents another device.
-        //!
-        //! Each entry is a map from the session_id to the pickled representation of the session.
-        lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE);
-        }
-
-        QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event)
-        {
-                if (!event.content.display_name.empty())
-                        return QString::fromStdString(event.content.display_name);
-
-                return QString::fromStdString(event.state_key);
-        }
-
-        std::optional<VerificationCache> verificationCache(const std::string &user_id,
-                                                           lmdb::txn &txn);
-        VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
-        std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn);
-
-        void setNextBatchToken(lmdb::txn &txn, const std::string &token);
-        void setNextBatchToken(lmdb::txn &txn, const QString &token);
-
-        lmdb::env env_;
-        lmdb::dbi syncStateDb_;
-        lmdb::dbi roomsDb_;
-        lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
-        lmdb::dbi invitesDb_;
-        lmdb::dbi readReceiptsDb_;
-        lmdb::dbi notificationsDb_;
-
-        lmdb::dbi devicesDb_;
-        lmdb::dbi deviceKeysDb_;
-
-        lmdb::dbi inboundMegolmSessionDb_;
-        lmdb::dbi outboundMegolmSessionDb_;
-        lmdb::dbi megolmSessionDataDb_;
-
-        lmdb::dbi encryptedRooms_;
-
-        QString localUserId_;
-        QString cacheDirectory_;
-
-        VerificationStorage verification_storage;
-
-        bool databaseReady_ = false;
+    }
+
+    void updateSpaces(lmdb::txn &txn,
+                      const std::set<std::string> &spaces_with_updates,
+                      std::set<std::string> rooms_with_updates);
+
+    lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
+    {
+        return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
+    }
+
+    lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+    }
+
+    // inverse of EventOrderDb
+    lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+    }
+
+    lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+    }
+
+    lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
+    }
+
+    lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        auto db = lmdb::dbi::open(
+          txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT);
+        lmdb::dbi_set_dupsort(txn, db, compare_state_key);
+        return db;
+    }
+
+    lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE);
+    }
+
+    lmdb::dbi getPresenceDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "presence", MDB_CREATE); }
+
+    lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); }
+
+    lmdb::dbi getVerificationDb(lmdb::txn &txn)
+    {
+        return lmdb::dbi::open(txn, "verified", MDB_CREATE);
+    }
+
+    //! Retrieves or creates the database that stores the open OLM sessions between our device
+    //! and the given curve25519 key which represents another device.
+    //!
+    //! Each entry is a map from the session_id to the pickled representation of the session.
+    lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key)
+    {
+        return lmdb::dbi::open(
+          txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE);
+    }
+
+    QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event)
+    {
+        if (!event.content.display_name.empty())
+            return QString::fromStdString(event.content.display_name);
+
+        return QString::fromStdString(event.state_key);
+    }
+
+    std::optional<VerificationCache> verificationCache(const std::string &user_id, lmdb::txn &txn);
+    VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
+    std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn);
+
+    void setNextBatchToken(lmdb::txn &txn, const std::string &token);
+
+    lmdb::env env_;
+    lmdb::dbi syncStateDb_;
+    lmdb::dbi roomsDb_;
+    lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
+    lmdb::dbi invitesDb_;
+    lmdb::dbi readReceiptsDb_;
+    lmdb::dbi notificationsDb_;
+
+    lmdb::dbi devicesDb_;
+    lmdb::dbi deviceKeysDb_;
+
+    lmdb::dbi inboundMegolmSessionDb_;
+    lmdb::dbi outboundMegolmSessionDb_;
+    lmdb::dbi megolmSessionDataDb_;
+
+    lmdb::dbi encryptedRooms_;
+
+    QString localUserId_;
+    QString cacheDirectory_;
+
+    std::string pickle_secret_;
+
+    VerificationStorage verification_storage;
+
+    bool databaseReady_ = false;
 };
 
 namespace cache {
diff --git a/src/CallDevices.cpp b/src/CallDevices.cpp
deleted file mode 100644
index 825d2f728b4242631ec0b663246293a241ac5285..0000000000000000000000000000000000000000
--- a/src/CallDevices.cpp
+++ /dev/null
@@ -1,396 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <cstring>
-#include <optional>
-#include <string_view>
-
-#include "CallDevices.h"
-#include "ChatPage.h"
-#include "Logging.h"
-#include "UserSettingsPage.h"
-
-#ifdef GSTREAMER_AVAILABLE
-extern "C"
-{
-#include "gst/gst.h"
-}
-#endif
-
-CallDevices::CallDevices()
-  : QObject()
-{}
-
-#ifdef GSTREAMER_AVAILABLE
-namespace {
-
-struct AudioSource
-{
-        std::string name;
-        GstDevice *device;
-};
-
-struct VideoSource
-{
-        struct Caps
-        {
-                std::string resolution;
-                std::vector<std::string> frameRates;
-        };
-        std::string name;
-        GstDevice *device;
-        std::vector<Caps> caps;
-};
-
-std::vector<AudioSource> audioSources_;
-std::vector<VideoSource> videoSources_;
-
-using FrameRate = std::pair<int, int>;
-std::optional<FrameRate>
-getFrameRate(const GValue *value)
-{
-        if (GST_VALUE_HOLDS_FRACTION(value)) {
-                gint num = gst_value_get_fraction_numerator(value);
-                gint den = gst_value_get_fraction_denominator(value);
-                return FrameRate{num, den};
-        }
-        return std::nullopt;
-}
-
-void
-addFrameRate(std::vector<std::string> &rates, const FrameRate &rate)
-{
-        constexpr double minimumFrameRate = 15.0;
-        if (static_cast<double>(rate.first) / rate.second >= minimumFrameRate)
-                rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second));
-}
-
-void
-setDefaultDevice(bool isVideo)
-{
-        auto settings = ChatPage::instance()->userSettings();
-        if (isVideo && settings->camera().isEmpty()) {
-                const VideoSource &camera = videoSources_.front();
-                settings->setCamera(QString::fromStdString(camera.name));
-                settings->setCameraResolution(
-                  QString::fromStdString(camera.caps.front().resolution));
-                settings->setCameraFrameRate(
-                  QString::fromStdString(camera.caps.front().frameRates.front()));
-        } else if (!isVideo && settings->microphone().isEmpty()) {
-                settings->setMicrophone(QString::fromStdString(audioSources_.front().name));
-        }
-}
-
-void
-addDevice(GstDevice *device)
-{
-        if (!device)
-                return;
-
-        gchar *name  = gst_device_get_display_name(device);
-        gchar *type  = gst_device_get_device_class(device);
-        bool isVideo = !std::strncmp(type, "Video", 5);
-        g_free(type);
-        nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name);
-        if (!isVideo) {
-                audioSources_.push_back({name, device});
-                g_free(name);
-                setDefaultDevice(false);
-                return;
-        }
-
-        GstCaps *gstcaps = gst_device_get_caps(device);
-        if (!gstcaps) {
-                nhlog::ui()->debug("WebRTC: unable to get caps for {}", name);
-                g_free(name);
-                return;
-        }
-
-        VideoSource source{name, device, {}};
-        g_free(name);
-        guint nCaps = gst_caps_get_size(gstcaps);
-        for (guint i = 0; i < nCaps; ++i) {
-                GstStructure *structure  = gst_caps_get_structure(gstcaps, i);
-                const gchar *struct_name = gst_structure_get_name(structure);
-                if (!std::strcmp(struct_name, "video/x-raw")) {
-                        gint widthpx, heightpx;
-                        if (gst_structure_get(structure,
-                                              "width",
-                                              G_TYPE_INT,
-                                              &widthpx,
-                                              "height",
-                                              G_TYPE_INT,
-                                              &heightpx,
-                                              nullptr)) {
-                                VideoSource::Caps caps;
-                                caps.resolution =
-                                  std::to_string(widthpx) + "x" + std::to_string(heightpx);
-                                const GValue *value =
-                                  gst_structure_get_value(structure, "framerate");
-                                if (auto fr = getFrameRate(value); fr)
-                                        addFrameRate(caps.frameRates, *fr);
-                                else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) {
-                                        addFrameRate(
-                                          caps.frameRates,
-                                          *getFrameRate(gst_value_get_fraction_range_min(value)));
-                                        addFrameRate(
-                                          caps.frameRates,
-                                          *getFrameRate(gst_value_get_fraction_range_max(value)));
-                                } else if (GST_VALUE_HOLDS_LIST(value)) {
-                                        guint nRates = gst_value_list_get_size(value);
-                                        for (guint j = 0; j < nRates; ++j) {
-                                                const GValue *rate =
-                                                  gst_value_list_get_value(value, j);
-                                                if (auto frate = getFrameRate(rate); frate)
-                                                        addFrameRate(caps.frameRates, *frate);
-                                        }
-                                }
-                                if (!caps.frameRates.empty())
-                                        source.caps.push_back(std::move(caps));
-                        }
-                }
-        }
-        gst_caps_unref(gstcaps);
-        videoSources_.push_back(std::move(source));
-        setDefaultDevice(true);
-}
-
-template<typename T>
-bool
-removeDevice(T &sources, GstDevice *device, bool changed)
-{
-        if (auto it = std::find_if(sources.begin(),
-                                   sources.end(),
-                                   [device](const auto &s) { return s.device == device; });
-            it != sources.end()) {
-                nhlog::ui()->debug(std::string("WebRTC: device ") +
-                                     (changed ? "changed: " : "removed: ") + "{}",
-                                   it->name);
-                gst_object_unref(device);
-                sources.erase(it);
-                return true;
-        }
-        return false;
-}
-
-void
-removeDevice(GstDevice *device, bool changed)
-{
-        if (device) {
-                if (removeDevice(audioSources_, device, changed) ||
-                    removeDevice(videoSources_, device, changed))
-                        return;
-        }
-}
-
-gboolean
-newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data G_GNUC_UNUSED)
-{
-        switch (GST_MESSAGE_TYPE(msg)) {
-        case GST_MESSAGE_DEVICE_ADDED: {
-                GstDevice *device;
-                gst_message_parse_device_added(msg, &device);
-                addDevice(device);
-                emit CallDevices::instance().devicesChanged();
-                break;
-        }
-        case GST_MESSAGE_DEVICE_REMOVED: {
-                GstDevice *device;
-                gst_message_parse_device_removed(msg, &device);
-                removeDevice(device, false);
-                emit CallDevices::instance().devicesChanged();
-                break;
-        }
-        case GST_MESSAGE_DEVICE_CHANGED: {
-                GstDevice *device;
-                GstDevice *oldDevice;
-                gst_message_parse_device_changed(msg, &device, &oldDevice);
-                removeDevice(oldDevice, true);
-                addDevice(device);
-                break;
-        }
-        default:
-                break;
-        }
-        return TRUE;
-}
-
-template<typename T>
-std::vector<std::string>
-deviceNames(T &sources, const std::string &defaultDevice)
-{
-        std::vector<std::string> ret;
-        ret.reserve(sources.size());
-        for (const auto &s : sources)
-                ret.push_back(s.name);
-
-        // move default device to top of the list
-        if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end())
-                std::swap(ret.front(), *it);
-
-        return ret;
-}
-
-std::optional<VideoSource>
-getVideoSource(const std::string &cameraName)
-{
-        if (auto it = std::find_if(videoSources_.cbegin(),
-                                   videoSources_.cend(),
-                                   [&cameraName](const auto &s) { return s.name == cameraName; });
-            it != videoSources_.cend()) {
-                return *it;
-        }
-        return std::nullopt;
-}
-
-std::pair<int, int>
-tokenise(std::string_view str, char delim)
-{
-        std::pair<int, int> ret;
-        ret.first  = std::atoi(str.data());
-        auto pos   = str.find_first_of(delim);
-        ret.second = std::atoi(str.data() + pos + 1);
-        return ret;
-}
-}
-
-void
-CallDevices::init()
-{
-        static GstDeviceMonitor *monitor = nullptr;
-        if (!monitor) {
-                monitor       = gst_device_monitor_new();
-                GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
-                gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
-                gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps);
-                gst_caps_unref(caps);
-                caps = gst_caps_new_empty_simple("video/x-raw");
-                gst_device_monitor_add_filter(monitor, "Video/Source", caps);
-                gst_device_monitor_add_filter(monitor, "Video/Duplex", caps);
-                gst_caps_unref(caps);
-
-                GstBus *bus = gst_device_monitor_get_bus(monitor);
-                gst_bus_add_watch(bus, newBusMessage, nullptr);
-                gst_object_unref(bus);
-                if (!gst_device_monitor_start(monitor)) {
-                        nhlog::ui()->error("WebRTC: failed to start device monitor");
-                        return;
-                }
-        }
-}
-
-bool
-CallDevices::haveMic() const
-{
-        return !audioSources_.empty();
-}
-
-bool
-CallDevices::haveCamera() const
-{
-        return !videoSources_.empty();
-}
-
-std::vector<std::string>
-CallDevices::names(bool isVideo, const std::string &defaultDevice) const
-{
-        return isVideo ? deviceNames(videoSources_, defaultDevice)
-                       : deviceNames(audioSources_, defaultDevice);
-}
-
-std::vector<std::string>
-CallDevices::resolutions(const std::string &cameraName) const
-{
-        std::vector<std::string> ret;
-        if (auto s = getVideoSource(cameraName); s) {
-                ret.reserve(s->caps.size());
-                for (const auto &c : s->caps)
-                        ret.push_back(c.resolution);
-        }
-        return ret;
-}
-
-std::vector<std::string>
-CallDevices::frameRates(const std::string &cameraName, const std::string &resolution) const
-{
-        if (auto s = getVideoSource(cameraName); s) {
-                if (auto it =
-                      std::find_if(s->caps.cbegin(),
-                                   s->caps.cend(),
-                                   [&](const auto &c) { return c.resolution == resolution; });
-                    it != s->caps.cend())
-                        return it->frameRates;
-        }
-        return {};
-}
-
-GstDevice *
-CallDevices::audioDevice() const
-{
-        std::string name = ChatPage::instance()->userSettings()->microphone().toStdString();
-        if (auto it = std::find_if(audioSources_.cbegin(),
-                                   audioSources_.cend(),
-                                   [&name](const auto &s) { return s.name == name; });
-            it != audioSources_.cend()) {
-                nhlog::ui()->debug("WebRTC: microphone: {}", name);
-                return it->device;
-        } else {
-                nhlog::ui()->error("WebRTC: unknown microphone: {}", name);
-                return nullptr;
-        }
-}
-
-GstDevice *
-CallDevices::videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const
-{
-        auto settings    = ChatPage::instance()->userSettings();
-        std::string name = settings->camera().toStdString();
-        if (auto s = getVideoSource(name); s) {
-                nhlog::ui()->debug("WebRTC: camera: {}", name);
-                resolution = tokenise(settings->cameraResolution().toStdString(), 'x');
-                frameRate  = tokenise(settings->cameraFrameRate().toStdString(), '/');
-                nhlog::ui()->debug(
-                  "WebRTC: camera resolution: {}x{}", resolution.first, resolution.second);
-                nhlog::ui()->debug(
-                  "WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second);
-                return s->device;
-        } else {
-                nhlog::ui()->error("WebRTC: unknown camera: {}", name);
-                return nullptr;
-        }
-}
-
-#else
-
-bool
-CallDevices::haveMic() const
-{
-        return false;
-}
-
-bool
-CallDevices::haveCamera() const
-{
-        return false;
-}
-
-std::vector<std::string>
-CallDevices::names(bool, const std::string &) const
-{
-        return {};
-}
-
-std::vector<std::string>
-CallDevices::resolutions(const std::string &) const
-{
-        return {};
-}
-
-std::vector<std::string>
-CallDevices::frameRates(const std::string &, const std::string &) const
-{
-        return {};
-}
-
-#endif
diff --git a/src/CallDevices.h b/src/CallDevices.h
deleted file mode 100644
index 69325f97b2c2070b5ffda9bb427c9d6dd45d37e7..0000000000000000000000000000000000000000
--- a/src/CallDevices.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <string>
-#include <utility>
-#include <vector>
-
-#include <QObject>
-
-typedef struct _GstDevice GstDevice;
-
-class CallDevices : public QObject
-{
-        Q_OBJECT
-
-public:
-        static CallDevices &instance()
-        {
-                static CallDevices instance;
-                return instance;
-        }
-
-        bool haveMic() const;
-        bool haveCamera() const;
-        std::vector<std::string> names(bool isVideo, const std::string &defaultDevice) const;
-        std::vector<std::string> resolutions(const std::string &cameraName) const;
-        std::vector<std::string> frameRates(const std::string &cameraName,
-                                            const std::string &resolution) const;
-
-signals:
-        void devicesChanged();
-
-private:
-        CallDevices();
-
-        friend class WebRTCSession;
-        void init();
-        GstDevice *audioDevice() const;
-        GstDevice *videoDevice(std::pair<int, int> &resolution,
-                               std::pair<int, int> &frameRate) const;
-
-public:
-        CallDevices(CallDevices const &) = delete;
-        void operator=(CallDevices const &) = delete;
-};
diff --git a/src/CallManager.cpp b/src/CallManager.cpp
deleted file mode 100644
index 6d41f1c6fc727107c9853dc778329fcec56553e4..0000000000000000000000000000000000000000
--- a/src/CallManager.cpp
+++ /dev/null
@@ -1,689 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <algorithm>
-#include <cctype>
-#include <chrono>
-#include <cstdint>
-#include <cstdlib>
-#include <memory>
-
-#include <QMediaPlaylist>
-#include <QUrl>
-
-#include "Cache.h"
-#include "CallDevices.h"
-#include "CallManager.h"
-#include "ChatPage.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-
-#include "mtx/responses/turn_server.hpp"
-
-#ifdef XCB_AVAILABLE
-#include <xcb/xcb.h>
-#include <xcb/xcb_ewmh.h>
-#endif
-
-#ifdef GSTREAMER_AVAILABLE
-extern "C"
-{
-#include "gst/gst.h"
-}
-#endif
-
-Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
-Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
-Q_DECLARE_METATYPE(mtx::responses::TurnServer)
-
-using namespace mtx::events;
-using namespace mtx::events::msg;
-
-using webrtc::CallType;
-
-namespace {
-std::vector<std::string>
-getTurnURIs(const mtx::responses::TurnServer &turnServer);
-}
-
-CallManager::CallManager(QObject *parent)
-  : QObject(parent)
-  , session_(WebRTCSession::instance())
-  , turnServerTimer_(this)
-{
-        qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
-        qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
-        qRegisterMetaType<mtx::responses::TurnServer>();
-
-        connect(
-          &session_,
-          &WebRTCSession::offerCreated,
-          this,
-          [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
-                  nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
-                  emit newMessage(roomid_, CallInvite{callid_, sdp, "0", timeoutms_});
-                  emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
-                  std::string callid(callid_);
-                  QTimer::singleShot(timeoutms_, this, [this, callid]() {
-                          if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) {
-                                  hangUp(CallHangUp::Reason::InviteTimeOut);
-                                  emit ChatPage::instance()->showNotification(
-                                    "The remote side failed to pick up.");
-                          }
-                  });
-          });
-
-        connect(
-          &session_,
-          &WebRTCSession::answerCreated,
-          this,
-          [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
-                  nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
-                  emit newMessage(roomid_, CallAnswer{callid_, sdp, "0"});
-                  emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
-          });
-
-        connect(&session_,
-                &WebRTCSession::newICECandidate,
-                this,
-                [this](const CallCandidates::Candidate &candidate) {
-                        nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
-                        emit newMessage(roomid_, CallCandidates{callid_, {candidate}, "0"});
-                });
-
-        connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
-
-        connect(this,
-                &CallManager::turnServerRetrieved,
-                this,
-                [this](const mtx::responses::TurnServer &res) {
-                        nhlog::net()->info("TURN server(s) retrieved from homeserver:");
-                        nhlog::net()->info("username: {}", res.username);
-                        nhlog::net()->info("ttl: {} seconds", res.ttl);
-                        for (const auto &u : res.uris)
-                                nhlog::net()->info("uri: {}", u);
-
-                        // Request new credentials close to expiry
-                        // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
-                        turnURIs_    = getTurnURIs(res);
-                        uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
-                        if (res.ttl < 3600)
-                                nhlog::net()->warn("Setting ttl to 1 hour");
-                        turnServerTimer_.setInterval(ttl * 1000 * 0.9);
-                });
-
-        connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
-                switch (state) {
-                case webrtc::State::DISCONNECTED:
-                        playRingtone(QUrl("qrc:/media/media/callend.ogg"), false);
-                        clear();
-                        break;
-                case webrtc::State::ICEFAILED: {
-                        QString error("Call connection failed.");
-                        if (turnURIs_.empty())
-                                error += " Your homeserver has no configured TURN server.";
-                        emit ChatPage::instance()->showNotification(error);
-                        hangUp(CallHangUp::Reason::ICEFailed);
-                        break;
-                }
-                default:
-                        break;
-                }
-                emit newCallState();
-        });
-
-        connect(&CallDevices::instance(),
-                &CallDevices::devicesChanged,
-                this,
-                &CallManager::devicesChanged);
-
-        connect(&player_,
-                &QMediaPlayer::mediaStatusChanged,
-                this,
-                [this](QMediaPlayer::MediaStatus status) {
-                        if (status == QMediaPlayer::LoadedMedia)
-                                player_.play();
-                });
-
-        connect(&player_,
-                QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error),
-                [this](QMediaPlayer::Error error) {
-                        stopRingtone();
-                        switch (error) {
-                        case QMediaPlayer::FormatError:
-                        case QMediaPlayer::ResourceError:
-                                nhlog::ui()->error("WebRTC: valid ringtone file not found");
-                                break;
-                        case QMediaPlayer::AccessDeniedError:
-                                nhlog::ui()->error("WebRTC: access to ringtone file denied");
-                                break;
-                        default:
-                                nhlog::ui()->error("WebRTC: unable to play ringtone");
-                                break;
-                        }
-                });
-}
-
-void
-CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
-{
-        if (isOnCall())
-                return;
-        if (callType == CallType::SCREEN) {
-                if (!screenShareSupported())
-                        return;
-                if (windows_.empty() || windowIndex >= windows_.size()) {
-                        nhlog::ui()->error("WebRTC: window index out of range");
-                        return;
-                }
-        }
-
-        auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
-        if (roomInfo.member_count != 2) {
-                emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms.");
-                return;
-        }
-
-        std::string errorMessage;
-        if (!session_.havePlugins(false, &errorMessage) ||
-            ((callType == CallType::VIDEO || callType == CallType::SCREEN) &&
-             !session_.havePlugins(true, &errorMessage))) {
-                emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
-                return;
-        }
-
-        callType_ = callType;
-        roomid_   = roomid;
-        session_.setTurnServers(turnURIs_);
-        generateCallID();
-        std::string strCallType = callType_ == CallType::VOICE
-                                    ? "voice"
-                                    : (callType_ == CallType::VIDEO ? "video" : "screen");
-        nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType);
-        std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
-        const RoomMember &callee =
-          members.front().user_id == utils::localUser() ? members.back() : members.front();
-        callParty_          = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
-        callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
-        emit newInviteState();
-        playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true);
-        if (!session_.createOffer(
-              callType, callType == CallType::SCREEN ? windows_[windowIndex].second : 0)) {
-                emit ChatPage::instance()->showNotification("Problem setting up call.");
-                endCall();
-        }
-}
-
-namespace {
-std::string
-callHangUpReasonString(CallHangUp::Reason reason)
-{
-        switch (reason) {
-        case CallHangUp::Reason::ICEFailed:
-                return "ICE failed";
-        case CallHangUp::Reason::InviteTimeOut:
-                return "Invite time out";
-        default:
-                return "User";
-        }
-}
-}
-
-void
-CallManager::hangUp(CallHangUp::Reason reason)
-{
-        if (!callid_.empty()) {
-                nhlog::ui()->debug(
-                  "WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
-                emit newMessage(roomid_, CallHangUp{callid_, "0", reason});
-                endCall();
-        }
-}
-
-void
-CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
-{
-#ifdef GSTREAMER_AVAILABLE
-        if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) ||
-            handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event))
-                return;
-#else
-        (void)event;
-#endif
-}
-
-template<typename T>
-bool
-CallManager::handleEvent(const mtx::events::collections::TimelineEvents &event)
-{
-        if (std::holds_alternative<RoomEvent<T>>(event)) {
-                handleEvent(std::get<RoomEvent<T>>(event));
-                return true;
-        }
-        return false;
-}
-
-void
-CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
-{
-        const char video[]     = "m=video";
-        const std::string &sdp = callInviteEvent.content.sdp;
-        bool isVideo           = std::search(sdp.cbegin(),
-                                   sdp.cend(),
-                                   std::cbegin(video),
-                                   std::cend(video) - 1,
-                                   [](unsigned char c1, unsigned char c2) {
-                                           return std::tolower(c1) == std::tolower(c2);
-                                   }) != sdp.cend();
-
-        nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
-                           callInviteEvent.content.call_id,
-                           (isVideo ? "video" : "voice"),
-                           callInviteEvent.sender);
-
-        if (callInviteEvent.content.call_id.empty())
-                return;
-
-        auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
-        if (isOnCall() || roomInfo.member_count != 2) {
-                emit newMessage(QString::fromStdString(callInviteEvent.room_id),
-                                CallHangUp{callInviteEvent.content.call_id,
-                                           "0",
-                                           CallHangUp::Reason::InviteTimeOut});
-                return;
-        }
-
-        const QString &ringtone = ChatPage::instance()->userSettings()->ringtone();
-        if (ringtone != "Mute")
-                playRingtone(ringtone == "Default" ? QUrl("qrc:/media/media/ring.ogg")
-                                                   : QUrl::fromLocalFile(ringtone),
-                             true);
-        roomid_ = QString::fromStdString(callInviteEvent.room_id);
-        callid_ = callInviteEvent.content.call_id;
-        remoteICECandidates_.clear();
-
-        std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
-        const RoomMember &caller =
-          members.front().user_id == utils::localUser() ? members.back() : members.front();
-        callParty_          = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
-        callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
-
-        haveCallInvite_ = true;
-        callType_       = isVideo ? CallType::VIDEO : CallType::VOICE;
-        inviteSDP_      = callInviteEvent.content.sdp;
-        emit newInviteState();
-}
-
-void
-CallManager::acceptInvite()
-{
-        if (!haveCallInvite_)
-                return;
-
-        stopRingtone();
-        std::string errorMessage;
-        if (!session_.havePlugins(false, &errorMessage) ||
-            (callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) {
-                emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
-                hangUp();
-                return;
-        }
-
-        session_.setTurnServers(turnURIs_);
-        if (!session_.acceptOffer(inviteSDP_)) {
-                emit ChatPage::instance()->showNotification("Problem setting up call.");
-                hangUp();
-                return;
-        }
-        session_.acceptICECandidates(remoteICECandidates_);
-        remoteICECandidates_.clear();
-        haveCallInvite_ = false;
-        emit newInviteState();
-}
-
-void
-CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
-{
-        if (callCandidatesEvent.sender == utils::localUser().toStdString())
-                return;
-
-        nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
-                           callCandidatesEvent.content.call_id,
-                           callCandidatesEvent.sender);
-
-        if (callid_ == callCandidatesEvent.content.call_id) {
-                if (isOnCall())
-                        session_.acceptICECandidates(callCandidatesEvent.content.candidates);
-                else {
-                        // CallInvite has been received and we're awaiting localUser to accept or
-                        // reject the call
-                        for (const auto &c : callCandidatesEvent.content.candidates)
-                                remoteICECandidates_.push_back(c);
-                }
-        }
-}
-
-void
-CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
-{
-        nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
-                           callAnswerEvent.content.call_id,
-                           callAnswerEvent.sender);
-
-        if (callAnswerEvent.sender == utils::localUser().toStdString() &&
-            callid_ == callAnswerEvent.content.call_id) {
-                if (!isOnCall()) {
-                        emit ChatPage::instance()->showNotification(
-                          "Call answered on another device.");
-                        stopRingtone();
-                        haveCallInvite_ = false;
-                        emit newInviteState();
-                }
-                return;
-        }
-
-        if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
-                stopRingtone();
-                if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
-                        emit ChatPage::instance()->showNotification("Problem setting up call.");
-                        hangUp();
-                }
-        }
-}
-
-void
-CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
-{
-        nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
-                           callHangUpEvent.content.call_id,
-                           callHangUpReasonString(callHangUpEvent.content.reason),
-                           callHangUpEvent.sender);
-
-        if (callid_ == callHangUpEvent.content.call_id)
-                endCall();
-}
-
-void
-CallManager::toggleMicMute()
-{
-        session_.toggleMicMute();
-        emit micMuteChanged();
-}
-
-bool
-CallManager::callsSupported()
-{
-#ifdef GSTREAMER_AVAILABLE
-        return true;
-#else
-        return false;
-#endif
-}
-
-bool
-CallManager::screenShareSupported()
-{
-        return std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY");
-}
-
-QStringList
-CallManager::devices(bool isVideo) const
-{
-        QStringList ret;
-        const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera()
-                                               : ChatPage::instance()->userSettings()->microphone();
-        std::vector<std::string> devices =
-          CallDevices::instance().names(isVideo, defaultDevice.toStdString());
-        ret.reserve(devices.size());
-        std::transform(devices.cbegin(),
-                       devices.cend(),
-                       std::back_inserter(ret),
-                       [](const auto &d) { return QString::fromStdString(d); });
-
-        return ret;
-}
-
-void
-CallManager::generateCallID()
-{
-        using namespace std::chrono;
-        uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
-        callid_     = "c" + std::to_string(ms);
-}
-
-void
-CallManager::clear()
-{
-        roomid_.clear();
-        callParty_.clear();
-        callPartyAvatarUrl_.clear();
-        callid_.clear();
-        callType_       = CallType::VOICE;
-        haveCallInvite_ = false;
-        emit newInviteState();
-        inviteSDP_.clear();
-        remoteICECandidates_.clear();
-}
-
-void
-CallManager::endCall()
-{
-        stopRingtone();
-        session_.end();
-        clear();
-}
-
-void
-CallManager::refreshTurnServer()
-{
-        turnURIs_.clear();
-        turnServerTimer_.start(2000);
-}
-
-void
-CallManager::retrieveTurnServer()
-{
-        http::client()->get_turn_server(
-          [this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          turnServerTimer_.setInterval(5000);
-                          return;
-                  }
-                  emit turnServerRetrieved(res);
-          });
-}
-
-void
-CallManager::playRingtone(const QUrl &ringtone, bool repeat)
-{
-        static QMediaPlaylist playlist;
-        playlist.clear();
-        playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
-                                        : QMediaPlaylist::CurrentItemOnce);
-        playlist.addMedia(ringtone);
-        player_.setVolume(100);
-        player_.setPlaylist(&playlist);
-}
-
-void
-CallManager::stopRingtone()
-{
-        player_.setPlaylist(nullptr);
-}
-
-QStringList
-CallManager::windowList()
-{
-        windows_.clear();
-        windows_.push_back({tr("Entire screen"), 0});
-
-#ifdef XCB_AVAILABLE
-        std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection(
-          xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); });
-        if (xcb_connection_has_error(connection.get())) {
-                nhlog::ui()->error("Failed to connect to X server");
-                return {};
-        }
-
-        xcb_ewmh_connection_t ewmh;
-        if (!xcb_ewmh_init_atoms_replies(
-              &ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) {
-                nhlog::ui()->error("Failed to connect to EWMH server");
-                return {};
-        }
-        std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>>
-          ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); });
-
-        for (int i = 0; i < ewmh.nb_screens; i++) {
-                xcb_ewmh_get_windows_reply_t clients;
-                if (!xcb_ewmh_get_client_list_reply(
-                      &ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) {
-                        nhlog::ui()->error("Failed to request window list");
-                        return {};
-                }
-
-                for (uint32_t w = 0; w < clients.windows_len; w++) {
-                        xcb_window_t window = clients.windows[w];
-
-                        std::string name;
-                        xcb_ewmh_get_utf8_strings_reply_t data;
-                        auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) {
-                                std::string name(r->strings, r->strings_len);
-                                xcb_ewmh_get_utf8_strings_reply_wipe(r);
-                                return name;
-                        };
-
-                        xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window);
-                        if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr))
-                                name = getName(&data);
-
-                        cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window);
-                        if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr))
-                                name = getName(&data);
-
-                        windows_.push_back({QString::fromStdString(name), window});
-                }
-                xcb_ewmh_get_windows_reply_wipe(&clients);
-        }
-#endif
-        QStringList ret;
-        ret.reserve(windows_.size());
-        for (const auto &w : windows_)
-                ret.append(w.first);
-
-        return ret;
-}
-
-#ifdef GSTREAMER_AVAILABLE
-namespace {
-
-GstElement *pipe_        = nullptr;
-unsigned int busWatchId_ = 0;
-
-gboolean
-newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer G_GNUC_UNUSED)
-{
-        switch (GST_MESSAGE_TYPE(msg)) {
-        case GST_MESSAGE_EOS:
-                if (pipe_) {
-                        gst_element_set_state(GST_ELEMENT(pipe_), GST_STATE_NULL);
-                        gst_object_unref(pipe_);
-                        pipe_ = nullptr;
-                }
-                if (busWatchId_) {
-                        g_source_remove(busWatchId_);
-                        busWatchId_ = 0;
-                }
-                break;
-        default:
-                break;
-        }
-        return TRUE;
-}
-}
-#endif
-
-void
-CallManager::previewWindow(unsigned int index) const
-{
-#ifdef GSTREAMER_AVAILABLE
-        if (windows_.empty() || index >= windows_.size() || !gst_is_initialized())
-                return;
-
-        GstElement *ximagesrc = gst_element_factory_make("ximagesrc", nullptr);
-        if (!ximagesrc) {
-                nhlog::ui()->error("Failed to create ximagesrc");
-                return;
-        }
-        GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
-        GstElement *videoscale   = gst_element_factory_make("videoscale", nullptr);
-        GstElement *capsfilter   = gst_element_factory_make("capsfilter", nullptr);
-        GstElement *ximagesink   = gst_element_factory_make("ximagesink", nullptr);
-
-        g_object_set(ximagesrc, "use-damage", FALSE, nullptr);
-        g_object_set(ximagesrc, "show-pointer", FALSE, nullptr);
-        g_object_set(ximagesrc, "xid", windows_[index].second, nullptr);
-
-        GstCaps *caps = gst_caps_new_simple(
-          "video/x-raw", "width", G_TYPE_INT, 480, "height", G_TYPE_INT, 360, nullptr);
-        g_object_set(capsfilter, "caps", caps, nullptr);
-        gst_caps_unref(caps);
-
-        pipe_ = gst_pipeline_new(nullptr);
-        gst_bin_add_many(
-          GST_BIN(pipe_), ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr);
-        if (!gst_element_link_many(
-              ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr)) {
-                nhlog::ui()->error("Failed to link preview window elements");
-                gst_object_unref(pipe_);
-                pipe_ = nullptr;
-                return;
-        }
-        if (gst_element_set_state(pipe_, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
-                nhlog::ui()->error("Unable to start preview pipeline");
-                gst_object_unref(pipe_);
-                pipe_ = nullptr;
-                return;
-        }
-
-        GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
-        busWatchId_ = gst_bus_add_watch(bus, newBusMessage, nullptr);
-        gst_object_unref(bus);
-#else
-        (void)index;
-#endif
-}
-
-namespace {
-std::vector<std::string>
-getTurnURIs(const mtx::responses::TurnServer &turnServer)
-{
-        // gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
-        // where username and password are percent-encoded
-        std::vector<std::string> ret;
-        for (const auto &uri : turnServer.uris) {
-                if (auto c = uri.find(':'); c == std::string::npos) {
-                        nhlog::ui()->error("Invalid TURN server uri: {}", uri);
-                        continue;
-                } else {
-                        std::string scheme = std::string(uri, 0, c);
-                        if (scheme != "turn" && scheme != "turns") {
-                                nhlog::ui()->error("Invalid TURN server uri: {}", uri);
-                                continue;
-                        }
-
-                        QString encodedUri =
-                          QString::fromStdString(scheme) + "://" +
-                          QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) +
-                          ":" +
-                          QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) +
-                          "@" + QString::fromStdString(std::string(uri, ++c));
-                        ret.push_back(encodedUri.toStdString());
-                }
-        }
-        return ret;
-}
-}
diff --git a/src/CallManager.h b/src/CallManager.h
deleted file mode 100644
index 1d9731916d334307779fa291c63ed021766b59fd..0000000000000000000000000000000000000000
--- a/src/CallManager.h
+++ /dev/null
@@ -1,115 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <string>
-#include <vector>
-
-#include <QMediaPlayer>
-#include <QObject>
-#include <QString>
-#include <QTimer>
-
-#include "CallDevices.h"
-#include "WebRTCSession.h"
-#include "mtx/events/collections.hpp"
-#include "mtx/events/voip.hpp"
-
-namespace mtx::responses {
-struct TurnServer;
-}
-
-class QStringList;
-class QUrl;
-
-class CallManager : public QObject
-{
-        Q_OBJECT
-        Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState)
-        Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
-        Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState)
-        Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState)
-        Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState)
-        Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState)
-        Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
-        Q_PROPERTY(bool haveLocalPiP READ haveLocalPiP NOTIFY newCallState)
-        Q_PROPERTY(QStringList mics READ mics NOTIFY devicesChanged)
-        Q_PROPERTY(QStringList cameras READ cameras NOTIFY devicesChanged)
-        Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
-        Q_PROPERTY(bool screenShareSupported READ screenShareSupported CONSTANT)
-
-public:
-        CallManager(QObject *);
-
-        bool haveCallInvite() const { return haveCallInvite_; }
-        bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; }
-        webrtc::CallType callType() const { return callType_; }
-        webrtc::State callState() const { return session_.state(); }
-        QString callParty() const { return callParty_; }
-        QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
-        bool isMicMuted() const { return session_.isMicMuted(); }
-        bool haveLocalPiP() const { return session_.haveLocalPiP(); }
-        QStringList mics() const { return devices(false); }
-        QStringList cameras() const { return devices(true); }
-        void refreshTurnServer();
-
-        static bool callsSupported();
-        static bool screenShareSupported();
-
-public slots:
-        void sendInvite(const QString &roomid, webrtc::CallType, unsigned int windowIndex = 0);
-        void syncEvent(const mtx::events::collections::TimelineEvents &event);
-        void toggleMicMute();
-        void toggleLocalPiP() { session_.toggleLocalPiP(); }
-        void acceptInvite();
-        void hangUp(
-          mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
-        QStringList windowList();
-        void previewWindow(unsigned int windowIndex) const;
-
-signals:
-        void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
-        void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
-        void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
-        void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
-        void newInviteState();
-        void newCallState();
-        void micMuteChanged();
-        void devicesChanged();
-        void turnServerRetrieved(const mtx::responses::TurnServer &);
-
-private slots:
-        void retrieveTurnServer();
-
-private:
-        WebRTCSession &session_;
-        QString roomid_;
-        QString callParty_;
-        QString callPartyAvatarUrl_;
-        std::string callid_;
-        const uint32_t timeoutms_  = 120000;
-        webrtc::CallType callType_ = webrtc::CallType::VOICE;
-        bool haveCallInvite_       = false;
-        std::string inviteSDP_;
-        std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_;
-        std::vector<std::string> turnURIs_;
-        QTimer turnServerTimer_;
-        QMediaPlayer player_;
-        std::vector<std::pair<QString, uint32_t>> windows_;
-
-        template<typename T>
-        bool handleEvent(const mtx::events::collections::TimelineEvents &event);
-        void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
-        void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
-        void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
-        void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
-        void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo);
-        void generateCallID();
-        QStringList devices(bool isVideo) const;
-        void clear();
-        void endCall();
-        void playRingtone(const QUrl &ringtone, bool repeat);
-        void stopRingtone();
-};
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 8a0e891b3b9c8e9c594f49458cf4370ee720cc69..d262387ccb2280fc9614bee039ebdcff23d2114a 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -6,26 +6,25 @@
 #include <QApplication>
 #include <QInputDialog>
 #include <QMessageBox>
-#include <QSettings>
 
 #include <mtx/responses.hpp>
 
 #include "AvatarProvider.h"
 #include "Cache.h"
 #include "Cache_p.h"
-#include "CallManager.h"
 #include "ChatPage.h"
-#include "DeviceVerificationFlow.h"
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
-#include "Olm.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "encryption/DeviceVerificationFlow.h"
+#include "encryption/Olm.h"
 #include "ui/OverlayModal.h"
 #include "ui/Theme.h"
 #include "ui/UserProfile.h"
+#include "voip/CallManager.h"
 
 #include "notifications/Manager.h"
 
@@ -33,9 +32,6 @@
 
 #include "blurhash.hpp"
 
-// TODO: Needs to be updated with an actual secret.
-static const std::string STORAGE_SECRET_KEY("secret");
-
 ChatPage *ChatPage::instance_             = nullptr;
 constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
 constexpr int RETRY_TIMEOUT               = 5'000;
@@ -54,643 +50,641 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
   , notificationsManager(this)
   , callManager_(new CallManager(this))
 {
-        setObjectName("chatPage");
-
-        instance_ = this;
-
-        qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>();
-        qRegisterMetaType<std::optional<RelatedInfo>>();
-        qRegisterMetaType<mtx::presence::PresenceState>();
-        qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>();
-        qRegisterMetaType<SecretsToDecrypt>();
-
-        topLayout_ = new QHBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setMargin(0);
-
-        view_manager_ = new TimelineViewManager(callManager_, this);
-
-        topLayout_->addWidget(view_manager_->getWidget());
-
-        connect(this,
-                &ChatPage::downloadedSecrets,
-                this,
-                &ChatPage::decryptDownloadedSecrets,
-                Qt::QueuedConnection);
-
-        connect(this, &ChatPage::connectionLost, this, [this]() {
-                nhlog::net()->info("connectivity lost");
-                isConnected_ = false;
-                http::client()->shutdown();
-        });
-        connect(this, &ChatPage::connectionRestored, this, [this]() {
-                nhlog::net()->info("trying to re-connect");
-                isConnected_ = true;
-
-                // Drop all pending connections.
-                http::client()->shutdown();
-                trySync();
-        });
-
-        connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
-        connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
-                if (http::client()->access_token().empty()) {
-                        connectivityTimer_.stop();
-                        return;
-                }
+    setObjectName("chatPage");
 
-                http::client()->versions(
-                  [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
-                          if (err) {
-                                  emit connectionLost();
-                                  return;
-                          }
+    instance_ = this;
 
-                          if (!isConnected_)
-                                  emit connectionRestored();
-                  });
-        });
+    qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>();
+    qRegisterMetaType<std::optional<RelatedInfo>>();
+    qRegisterMetaType<mtx::presence::PresenceState>();
+    qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>();
+    qRegisterMetaType<SecretsToDecrypt>();
 
-        connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
+    topLayout_ = new QHBoxLayout(this);
+    topLayout_->setSpacing(0);
+    topLayout_->setMargin(0);
 
-        connect(
-          view_manager_,
-          &TimelineViewManager::inviteUsers,
-          this,
-          [this](QString roomId, QStringList users) {
-                  for (int ii = 0; ii < users.size(); ++ii) {
-                          QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
-                                  const auto user = users.at(ii);
-
-                                  http::client()->invite_user(
-                                    roomId.toStdString(),
-                                    user.toStdString(),
-                                    [this, user](const mtx::responses::RoomInvite &,
-                                                 mtx::http::RequestErr err) {
-                                            if (err) {
-                                                    emit showNotification(
-                                                      tr("Failed to invite user: %1").arg(user));
-                                                    return;
-                                            }
-
-                                            emit showNotification(tr("Invited user: %1").arg(user));
-                                    });
-                          });
-                  }
-          });
+    view_manager_ = new TimelineViewManager(callManager_, this);
 
-        connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
-        connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
-        connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
-        connect(this,
-                &ChatPage::highlightedNotifsRetrieved,
-                this,
-                [](const mtx::responses::Notifications &notif) {
-                        try {
-                                cache::saveTimelineMentions(notif);
-                        } catch (const lmdb::error &e) {
-                                nhlog::db()->error("failed to save mentions: {}", e.what());
-                        }
-                });
-
-        connect(&notificationsManager,
-                &NotificationsManager::notificationClicked,
-                this,
-                [this](const QString &roomid, const QString &eventid) {
-                        Q_UNUSED(eventid)
-                        view_manager_->rooms()->setCurrentRoom(roomid);
-                        activateWindow();
-                });
-        connect(&notificationsManager,
-                &NotificationsManager::sendNotificationReply,
-                this,
-                [this](const QString &roomid, const QString &eventid, const QString &body) {
-                        view_manager_->rooms()->setCurrentRoom(roomid);
-                        view_manager_->queueReply(roomid, eventid, body);
-                        activateWindow();
-                });
-
-        connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
-                // ensure the qml context is shutdown before we destroy all other singletons
-                // Otherwise Qml will try to access the room list or settings, after they have been
-                // destroyed
-                topLayout_->removeWidget(view_manager_->getWidget());
-                delete view_manager_->getWidget();
-        });
-
-        connect(
-          this,
-          &ChatPage::initializeViews,
-          view_manager_,
-          [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); },
-          Qt::QueuedConnection);
-        connect(this,
-                &ChatPage::initializeEmptyViews,
-                view_manager_,
-                &TimelineViewManager::initializeRoomlist);
-        connect(
-          this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
-        connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
-                view_manager_->sync(rooms);
-
-                bool hasNotifications = false;
-                for (const auto &room : rooms.join) {
-                        if (room.second.unread_notifications.notification_count > 0)
-                                hasNotifications = true;
-                }
+    topLayout_->addWidget(view_manager_->getWidget());
 
-                if (hasNotifications && userSettings_->hasNotifications())
-                        http::client()->notifications(
-                          5,
-                          "",
-                          "",
-                          [this](const mtx::responses::Notifications &res,
-                                 mtx::http::RequestErr err) {
-                                  if (err) {
-                                          nhlog::net()->warn(
-                                            "failed to retrieve notifications: {} ({})",
-                                            err->matrix_error.error,
-                                            static_cast<int>(err->status_code));
-                                          return;
-                                  }
-
-                                  emit notificationsRetrieved(std::move(res));
-                          });
-        });
-
-        connect(
-          this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
-        connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection);
-        connect(
-          this,
-          &ChatPage::tryDelayedSyncCb,
-          this,
-          [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
-          Qt::QueuedConnection);
+    connect(this,
+            &ChatPage::downloadedSecrets,
+            this,
+            &ChatPage::decryptDownloadedSecrets,
+            Qt::QueuedConnection);
 
-        connect(this,
-                &ChatPage::newSyncResponse,
-                this,
-                &ChatPage::handleSyncResponse,
-                Qt::QueuedConnection);
+    connect(this, &ChatPage::connectionLost, this, [this]() {
+        nhlog::net()->info("connectivity lost");
+        isConnected_ = false;
+        http::client()->shutdown();
+    });
+    connect(this, &ChatPage::connectionRestored, this, [this]() {
+        nhlog::net()->info("trying to re-connect");
+        isConnected_ = true;
+
+        // Drop all pending connections.
+        http::client()->shutdown();
+        trySync();
+    });
+
+    connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
+    connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
+        if (http::client()->access_token().empty()) {
+            connectivityTimer_.stop();
+            return;
+        }
 
-        connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
+        http::client()->versions(
+          [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+              if (err) {
+                  emit connectionLost();
+                  return;
+              }
 
-        connectCallMessage<mtx::events::msg::CallInvite>();
-        connectCallMessage<mtx::events::msg::CallCandidates>();
-        connectCallMessage<mtx::events::msg::CallAnswer>();
-        connectCallMessage<mtx::events::msg::CallHangUp>();
+              if (!isConnected_)
+                  emit connectionRestored();
+          });
+    });
+
+    connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
+
+    connect(
+      view_manager_,
+      &TimelineViewManager::inviteUsers,
+      this,
+      [this](QString roomId, QStringList users) {
+          for (int ii = 0; ii < users.size(); ++ii) {
+              QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
+                  const auto user = users.at(ii);
+
+                  http::client()->invite_user(
+                    roomId.toStdString(),
+                    user.toStdString(),
+                    [this, user](const mtx::responses::RoomInvite &, mtx::http::RequestErr err) {
+                        if (err) {
+                            emit showNotification(tr("Failed to invite user: %1").arg(user));
+                            return;
+                        }
+
+                        emit showNotification(tr("Invited user: %1").arg(user));
+                    });
+              });
+          }
+      });
+
+    connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
+    connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
+    connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
+    connect(this,
+            &ChatPage::highlightedNotifsRetrieved,
+            this,
+            [](const mtx::responses::Notifications &notif) {
+                try {
+                    cache::saveTimelineMentions(notif);
+                } catch (const lmdb::error &e) {
+                    nhlog::db()->error("failed to save mentions: {}", e.what());
+                }
+            });
+
+    connect(&notificationsManager,
+            &NotificationsManager::notificationClicked,
+            this,
+            [this](const QString &roomid, const QString &eventid) {
+                Q_UNUSED(eventid)
+                view_manager_->rooms()->setCurrentRoom(roomid);
+                activateWindow();
+            });
+    connect(&notificationsManager,
+            &NotificationsManager::sendNotificationReply,
+            this,
+            [this](const QString &roomid, const QString &eventid, const QString &body) {
+                view_manager_->rooms()->setCurrentRoom(roomid);
+                view_manager_->queueReply(roomid, eventid, body);
+                activateWindow();
+            });
+
+    connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
+        // ensure the qml context is shutdown before we destroy all other singletons
+        // Otherwise Qml will try to access the room list or settings, after they have been
+        // destroyed
+        topLayout_->removeWidget(view_manager_->getWidget());
+        delete view_manager_->getWidget();
+    });
+
+    connect(
+      this,
+      &ChatPage::initializeViews,
+      view_manager_,
+      [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); },
+      Qt::QueuedConnection);
+    connect(this,
+            &ChatPage::initializeEmptyViews,
+            view_manager_,
+            &TimelineViewManager::initializeRoomlist);
+    connect(
+      this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
+    connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
+        view_manager_->sync(rooms);
+
+        static unsigned int prevNotificationCount = 0;
+        unsigned int notificationCount            = 0;
+        for (const auto &room : rooms.join) {
+            notificationCount += room.second.unread_notifications.notification_count;
+        }
+
+        // HACK: If we had less notifications last time we checked, send an alert if the
+        // user wanted one. Technically, this may cause an alert to be missed if new ones
+        // come in while you are reading old ones. Since the window is almost certainly open
+        // in this edge case, that's probably a non-issue.
+        // TODO: Replace this once we have proper pushrules support. This is a horrible hack
+        if (prevNotificationCount < notificationCount) {
+            if (userSettings_->hasAlertOnNotification())
+                QApplication::alert(this);
+        }
+        prevNotificationCount = notificationCount;
+
+        // No need to check amounts for this section, as this function internally checks for
+        // duplicates.
+        if (notificationCount && userSettings_->hasNotifications())
+            http::client()->notifications(
+              5,
+              "",
+              "",
+              [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) {
+                  if (err) {
+                      nhlog::net()->warn("failed to retrieve notifications: {} ({})",
+                                         err->matrix_error.error,
+                                         static_cast<int>(err->status_code));
+                      return;
+                  }
+
+                  emit notificationsRetrieved(std::move(res));
+              });
+    });
+
+    connect(
+      this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
+    connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection);
+    connect(
+      this,
+      &ChatPage::tryDelayedSyncCb,
+      this,
+      [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
+      Qt::QueuedConnection);
+
+    connect(
+      this, &ChatPage::newSyncResponse, this, &ChatPage::handleSyncResponse, Qt::QueuedConnection);
+
+    connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
+
+    connectCallMessage<mtx::events::msg::CallInvite>();
+    connectCallMessage<mtx::events::msg::CallCandidates>();
+    connectCallMessage<mtx::events::msg::CallAnswer>();
+    connectCallMessage<mtx::events::msg::CallHangUp>();
 }
 
 void
 ChatPage::logout()
 {
-        resetUI();
-        deleteConfigs();
+    resetUI();
+    deleteConfigs();
 
-        emit closing();
-        connectivityTimer_.stop();
+    emit closing();
+    connectivityTimer_.stop();
 }
 
 void
 ChatPage::dropToLoginPage(const QString &msg)
 {
-        nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
+    nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
 
-        http::client()->shutdown();
-        connectivityTimer_.stop();
+    http::client()->shutdown();
+    connectivityTimer_.stop();
 
-        resetUI();
-        deleteConfigs();
+    resetUI();
+    deleteConfigs();
 
-        emit showLoginPage(msg);
+    emit showLoginPage(msg);
 }
 
 void
 ChatPage::resetUI()
 {
-        view_manager_->clearAll();
+    view_manager_->clearAll();
 
-        emit unreadMessages(0);
+    emit unreadMessages(0);
 }
 
 void
 ChatPage::deleteConfigs()
 {
-        QSettings settings;
-
-        if (UserSettings::instance()->profile() != "") {
-                settings.beginGroup("profile");
-                settings.beginGroup(UserSettings::instance()->profile());
-        }
-        settings.beginGroup("auth");
-        settings.remove("");
-        settings.endGroup(); // auth
-
-        http::client()->shutdown();
-        cache::deleteData();
+    auto settings = UserSettings::instance()->qsettings();
+
+    if (UserSettings::instance()->profile() != "") {
+        settings->beginGroup("profile");
+        settings->beginGroup(UserSettings::instance()->profile());
+    }
+    settings->beginGroup("auth");
+    settings->remove("");
+    settings->endGroup(); // auth
+
+    http::client()->shutdown();
+    cache::deleteData();
 }
 
 void
 ChatPage::bootstrap(QString userid, QString homeserver, QString token)
 {
-        using namespace mtx::identifiers;
+    using namespace mtx::identifiers;
 
-        try {
-                http::client()->set_user(parse<User>(userid.toStdString()));
-        } catch (const std::invalid_argument &) {
-                nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
-                                      userid.toStdString());
-        }
+    try {
+        http::client()->set_user(parse<User>(userid.toStdString()));
+    } catch (const std::invalid_argument &) {
+        nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString());
+    }
 
-        http::client()->set_server(homeserver.toStdString());
-        http::client()->set_access_token(token.toStdString());
-        http::client()->verify_certificates(
-          !UserSettings::instance()->disableCertificateValidation());
+    http::client()->set_server(homeserver.toStdString());
+    http::client()->set_access_token(token.toStdString());
+    http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
 
-        // The Olm client needs the user_id & device_id that will be included
-        // in the generated payloads & keys.
-        olm::client()->set_user_id(http::client()->user_id().to_string());
-        olm::client()->set_device_id(http::client()->device_id());
+    // The Olm client needs the user_id & device_id that will be included
+    // in the generated payloads & keys.
+    olm::client()->set_user_id(http::client()->user_id().to_string());
+    olm::client()->set_device_id(http::client()->device_id());
 
-        try {
-                cache::init(userid);
-
-                connect(cache::client(),
-                        &Cache::newReadReceipts,
-                        view_manager_,
-                        &TimelineViewManager::updateReadReceipts);
-
-                connect(cache::client(),
-                        &Cache::removeNotification,
-                        &notificationsManager,
-                        &NotificationsManager::removeNotification);
-
-                const bool isInitialized = cache::isInitialized();
-                const auto cacheVersion  = cache::formatVersion();
-
-                callManager_->refreshTurnServer();
-
-                if (!isInitialized) {
-                        cache::setCurrentFormat();
-                } else {
-                        if (cacheVersion == cache::CacheVersion::Current) {
-                                loadStateFromCache();
-                                return;
-                        } else if (cacheVersion == cache::CacheVersion::Older) {
-                                if (!cache::runMigrations()) {
-                                        QMessageBox::critical(
-                                          this,
+    try {
+        cache::init(userid);
+
+        connect(cache::client(),
+                &Cache::newReadReceipts,
+                view_manager_,
+                &TimelineViewManager::updateReadReceipts);
+
+        connect(cache::client(),
+                &Cache::removeNotification,
+                &notificationsManager,
+                &NotificationsManager::removeNotification);
+
+        const bool isInitialized = cache::isInitialized();
+        const auto cacheVersion  = cache::formatVersion();
+
+        callManager_->refreshTurnServer();
+
+        if (!isInitialized) {
+            cache::setCurrentFormat();
+        } else {
+            if (cacheVersion == cache::CacheVersion::Current) {
+                loadStateFromCache();
+                return;
+            } else if (cacheVersion == cache::CacheVersion::Older) {
+                if (!cache::runMigrations()) {
+                    QMessageBox::critical(this,
                                           tr("Cache migration failed!"),
                                           tr("Migrating the cache to the current version failed. "
                                              "This can have different reasons. Please open an "
                                              "issue and try to use an older version in the mean "
                                              "time. Alternatively you can try deleting the cache "
                                              "manually."));
-                                        QCoreApplication::quit();
-                                }
-                                loadStateFromCache();
-                                return;
-                        } else if (cacheVersion == cache::CacheVersion::Newer) {
-                                QMessageBox::critical(
-                                  this,
-                                  tr("Incompatible cache version"),
-                                  tr("The cache on your disk is newer than this version of Nheko "
-                                     "supports. Please update or clear your cache."));
-                                QCoreApplication::quit();
-                                return;
-                        }
+                    QCoreApplication::quit();
                 }
-
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failure during boot: {}", e.what());
-                cache::deleteData();
-                nhlog::net()->info("falling back to initial sync");
-        }
-
-        try {
-                // It's the first time syncing with this device
-                // There isn't a saved olm account to restore.
-                nhlog::crypto()->info("creating new olm account");
-                olm::client()->create_new_account();
-                cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY));
-        } catch (const lmdb::error &e) {
-                nhlog::crypto()->critical("failed to save olm account {}", e.what());
-                emit dropToLoginPageCb(QString::fromStdString(e.what()));
+                loadStateFromCache();
                 return;
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to create new olm account {}", e.what());
-                emit dropToLoginPageCb(QString::fromStdString(e.what()));
+            } else if (cacheVersion == cache::CacheVersion::Newer) {
+                QMessageBox::critical(
+                  this,
+                  tr("Incompatible cache version"),
+                  tr("The cache on your disk is newer than this version of Nheko "
+                     "supports. Please update or clear your cache."));
+                QCoreApplication::quit();
                 return;
+            }
         }
 
-        getProfileInfo();
-        tryInitialSync();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("failure during boot: {}", e.what());
+        cache::deleteData();
+        nhlog::net()->info("falling back to initial sync");
+    }
+
+    try {
+        // It's the first time syncing with this device
+        // There isn't a saved olm account to restore.
+        nhlog::crypto()->info("creating new olm account");
+        olm::client()->create_new_account();
+        cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret()));
+    } catch (const lmdb::error &e) {
+        nhlog::crypto()->critical("failed to save olm account {}", e.what());
+        emit dropToLoginPageCb(QString::fromStdString(e.what()));
+        return;
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to create new olm account {}", e.what());
+        emit dropToLoginPageCb(QString::fromStdString(e.what()));
+        return;
+    }
+
+    getProfileInfo();
+    getBackupVersion();
+    tryInitialSync();
 }
 
 void
 ChatPage::loadStateFromCache()
 {
-        nhlog::db()->info("restoring state from cache");
-
-        try {
-                olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
-
-                emit initializeEmptyViews();
-                emit initializeMentions(cache::getTimelineMentions());
-
-                cache::calculateRoomReadStatus();
-
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
-                emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again."));
-                return;
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to restore cache: {}", e.what());
-                emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
-                return;
-        } catch (const json::exception &e) {
-                nhlog::db()->critical("failed to parse cache data: {}", e.what());
-                return;
-        }
-
-        nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
-        nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
-
-        getProfileInfo();
-
-        emit contentLoaded();
-
-        // Start receiving events.
-        emit trySyncCb();
+    nhlog::db()->info("restoring state from cache");
+
+    try {
+        olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret());
+
+        emit initializeEmptyViews();
+        emit initializeMentions(cache::getTimelineMentions());
+
+        cache::calculateRoomReadStatus();
+
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again."));
+        return;
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("failed to restore cache: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
+        return;
+    } catch (const json::exception &e) {
+        nhlog::db()->critical("failed to parse cache data: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
+        return;
+    } catch (const std::exception &e) {
+        nhlog::db()->critical("failed to load cache data: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
+        return;
+    }
+
+    nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+    nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+
+    getProfileInfo();
+    getBackupVersion();
+    verifyOneTimeKeyCountAfterStartup();
+
+    emit contentLoaded();
+
+    // Start receiving events.
+    emit trySyncCb();
 }
 
 void
 ChatPage::removeRoom(const QString &room_id)
 {
-        try {
-                cache::removeRoom(room_id);
-                cache::removeInvite(room_id.toStdString());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failure while removing room: {}", e.what());
-                // TODO: Notify the user.
-        }
+    try {
+        cache::removeRoom(room_id);
+        cache::removeInvite(room_id.toStdString());
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("failure while removing room: {}", e.what());
+        // TODO: Notify the user.
+    }
 }
 
 void
 ChatPage::sendNotifications(const mtx::responses::Notifications &res)
 {
-        for (const auto &item : res.notifications) {
-                const auto event_id = mtx::accessors::event_id(item.event);
-
-                try {
-                        if (item.read) {
-                                cache::removeReadNotification(event_id);
-                                continue;
-                        }
-
-                        if (!cache::isNotificationSent(event_id)) {
-                                const auto room_id = QString::fromStdString(item.room_id);
-
-                                // We should only sent one notification per event.
-                                cache::markSentNotification(event_id);
+    for (const auto &item : res.notifications) {
+        const auto event_id = mtx::accessors::event_id(item.event);
 
-                                // Don't send a notification when the current room is opened.
-                                if (isRoomActive(room_id))
-                                        continue;
-
-                                if (userSettings_->hasAlertOnNotification()) {
-                                        QApplication::alert(this);
-                                }
-
-                                if (userSettings_->hasDesktopNotifications()) {
-                                        auto info = cache::singleRoomInfo(item.room_id);
-
-                                        AvatarProvider::resolve(
-                                          QString::fromStdString(info.avatar_url),
-                                          96,
-                                          this,
-                                          [this, item](QPixmap image) {
-                                                  notificationsManager.postNotification(
-                                                    item, image.toImage());
-                                          });
-                                }
-                        }
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->warn("error while sending notification: {}", e.what());
+        try {
+            if (item.read) {
+                cache::removeReadNotification(event_id);
+                continue;
+            }
+
+            if (!cache::isNotificationSent(event_id)) {
+                const auto room_id = QString::fromStdString(item.room_id);
+
+                // We should only sent one notification per event.
+                cache::markSentNotification(event_id);
+
+                // Don't send a notification when the current room is opened.
+                if (isRoomActive(room_id))
+                    continue;
+
+                if (userSettings_->hasDesktopNotifications()) {
+                    auto info = cache::singleRoomInfo(item.room_id);
+
+                    AvatarProvider::resolve(QString::fromStdString(info.avatar_url),
+                                            96,
+                                            this,
+                                            [this, item](QPixmap image) {
+                                                notificationsManager.postNotification(
+                                                  item, image.toImage());
+                                            });
                 }
+            }
+        } catch (const lmdb::error &e) {
+            nhlog::db()->warn("error while sending notification: {}", e.what());
         }
+    }
 }
 
 void
 ChatPage::tryInitialSync()
 {
-        nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
-        nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+    nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+    nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
 
-        // Upload one time keys for the device.
-        nhlog::crypto()->info("generating one time keys");
-        olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS);
+    // Upload one time keys for the device.
+    nhlog::crypto()->info("generating one time keys");
+    olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS);
 
-        http::client()->upload_keys(
-          olm::client()->create_upload_keys_request(),
-          [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          const int status_code = static_cast<int>(err->status_code);
+    http::client()->upload_keys(
+      olm::client()->create_upload_keys_request(),
+      [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
+          if (err) {
+              const int status_code = static_cast<int>(err->status_code);
 
-                          if (status_code == 404) {
-                                  nhlog::net()->warn(
-                                    "skipping key uploading. server doesn't provide /keys/upload");
-                                  return startInitialSync();
-                          }
+              if (status_code == 404) {
+                  nhlog::net()->warn("skipping key uploading. server doesn't provide /keys/upload");
+                  return startInitialSync();
+              }
 
-                          nhlog::crypto()->critical("failed to upload one time keys: {} {}",
-                                                    err->matrix_error.error,
-                                                    status_code);
+              nhlog::crypto()->critical(
+                "failed to upload one time keys: {} {}", err->matrix_error.error, status_code);
 
-                          QString errorMsg(tr("Failed to setup encryption keys. Server response: "
-                                              "%1 %2. Please try again later.")
-                                             .arg(QString::fromStdString(err->matrix_error.error))
-                                             .arg(status_code));
+              QString errorMsg(tr("Failed to setup encryption keys. Server response: "
+                                  "%1 %2. Please try again later.")
+                                 .arg(QString::fromStdString(err->matrix_error.error))
+                                 .arg(status_code));
 
-                          emit dropToLoginPageCb(errorMsg);
-                          return;
-                  }
+              emit dropToLoginPageCb(errorMsg);
+              return;
+          }
 
-                  olm::mark_keys_as_published();
+          olm::mark_keys_as_published();
 
-                  for (const auto &entry : res.one_time_key_counts)
-                          nhlog::net()->info(
-                            "uploaded {} {} one-time keys", entry.second, entry.first);
+          for (const auto &entry : res.one_time_key_counts)
+              nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first);
 
-                  startInitialSync();
-          });
+          startInitialSync();
+      });
 }
 
 void
 ChatPage::startInitialSync()
 {
-        nhlog::net()->info("trying initial sync");
-
-        mtx::http::SyncOpts opts;
-        opts.timeout      = 0;
-        opts.set_presence = currentPresence();
-
-        http::client()->sync(
-          opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
-                  // TODO: Initial Sync should include mentions as well...
+    nhlog::net()->info("trying initial sync");
+
+    mtx::http::SyncOpts opts;
+    opts.timeout      = 0;
+    opts.set_presence = currentPresence();
+
+    http::client()->sync(opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+        // TODO: Initial Sync should include mentions as well...
+
+        if (err) {
+            const auto error      = QString::fromStdString(err->matrix_error.error);
+            const auto msg        = tr("Please try to login again: %1").arg(error);
+            const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+            const int status_code = static_cast<int>(err->status_code);
+
+            nhlog::net()->error("initial sync error: {} {} {} {}",
+                                err->parse_error,
+                                status_code,
+                                err->error_code,
+                                err_code);
+
+            // non http related errors
+            if (status_code <= 0 || status_code >= 600) {
+                startInitialSync();
+                return;
+            }
 
-                  if (err) {
-                          const auto error      = QString::fromStdString(err->matrix_error.error);
-                          const auto msg        = tr("Please try to login again: %1").arg(error);
-                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
-                          const int status_code = static_cast<int>(err->status_code);
-
-                          nhlog::net()->error("initial sync error: {} {} {} {}",
-                                              err->parse_error,
-                                              status_code,
-                                              err->error_code,
-                                              err_code);
-
-                          // non http related errors
-                          if (status_code <= 0 || status_code >= 600) {
-                                  startInitialSync();
-                                  return;
-                          }
-
-                          switch (status_code) {
-                          case 502:
-                          case 504:
-                          case 524: {
-                                  startInitialSync();
-                                  return;
-                          }
-                          default: {
-                                  emit dropToLoginPageCb(msg);
-                                  return;
-                          }
-                          }
-                  }
+            switch (status_code) {
+            case 502:
+            case 504:
+            case 524: {
+                startInitialSync();
+                return;
+            }
+            default: {
+                emit dropToLoginPageCb(msg);
+                return;
+            }
+            }
+        }
 
-                  nhlog::net()->info("initial sync completed");
+        nhlog::net()->info("initial sync completed");
 
-                  try {
-                          cache::client()->saveState(res);
+        try {
+            cache::client()->saveState(res);
 
-                          olm::handle_to_device_messages(res.to_device.events);
+            olm::handle_to_device_messages(res.to_device.events);
 
-                          emit initializeViews(std::move(res.rooms));
-                          emit initializeMentions(cache::getTimelineMentions());
+            emit initializeViews(std::move(res.rooms));
+            emit initializeMentions(cache::getTimelineMentions());
 
-                          cache::calculateRoomReadStatus();
-                  } catch (const lmdb::error &e) {
-                          nhlog::db()->error("failed to save state after initial sync: {}",
-                                             e.what());
-                          startInitialSync();
-                          return;
-                  }
+            cache::calculateRoomReadStatus();
+        } catch (const lmdb::error &e) {
+            nhlog::db()->error("failed to save state after initial sync: {}", e.what());
+            startInitialSync();
+            return;
+        }
 
-                  emit trySyncCb();
-                  emit contentLoaded();
-          });
+        emit trySyncCb();
+        emit contentLoaded();
+    });
 }
 
 void
 ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token)
 {
-        try {
-                if (prev_batch_token != cache::nextBatchToken()) {
-                        nhlog::net()->warn("Duplicate sync, dropping");
-                        return;
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn("Logged out in the mean time, dropping sync");
+    try {
+        if (prev_batch_token != cache::nextBatchToken()) {
+            nhlog::net()->warn("Duplicate sync, dropping");
+            return;
         }
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("Logged out in the mean time, dropping sync");
+    }
 
-        nhlog::net()->debug("sync completed: {}", res.next_batch);
+    nhlog::net()->debug("sync completed: {}", res.next_batch);
 
-        // Ensure that we have enough one-time keys available.
-        ensureOneTimeKeyCount(res.device_one_time_keys_count);
+    // Ensure that we have enough one-time keys available.
+    ensureOneTimeKeyCount(res.device_one_time_keys_count);
 
-        // TODO: fine grained error handling
-        try {
-                cache::client()->saveState(res);
-                olm::handle_to_device_messages(res.to_device.events);
+    // TODO: fine grained error handling
+    try {
+        cache::client()->saveState(res);
+        olm::handle_to_device_messages(res.to_device.events);
 
-                auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
+        auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
 
-                emit syncUI(res.rooms);
+        emit syncUI(res.rooms);
 
-                // if we process a lot of syncs (1 every 200ms), this means we clean the
-                // db every 100s
-                static int syncCounter = 0;
-                if (syncCounter++ >= 500) {
-                        cache::deleteOldData();
-                        syncCounter = 0;
-                }
-        } catch (const lmdb::map_full_error &e) {
-                nhlog::db()->error("lmdb is full: {}", e.what());
-                cache::deleteOldData();
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("saving sync response: {}", e.what());
+        // if we process a lot of syncs (1 every 200ms), this means we clean the
+        // db every 100s
+        static int syncCounter = 0;
+        if (syncCounter++ >= 500) {
+            cache::deleteOldData();
+            syncCounter = 0;
         }
-
-        emit trySyncCb();
+    } catch (const lmdb::map_full_error &e) {
+        nhlog::db()->error("lmdb is full: {}", e.what());
+        cache::deleteOldData();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("saving sync response: {}", e.what());
+    }
+
+    emit trySyncCb();
 }
 
 void
 ChatPage::trySync()
 {
-        mtx::http::SyncOpts opts;
-        opts.set_presence = currentPresence();
-
-        if (!connectivityTimer_.isActive())
-                connectivityTimer_.start();
-
-        try {
-                opts.since = cache::nextBatchToken();
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("failed to retrieve next batch token: {}", e.what());
-                return;
-        }
-
-        http::client()->sync(
-          opts,
-          [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          const auto error      = QString::fromStdString(err->matrix_error.error);
-                          const auto msg        = tr("Please try to login again: %1").arg(error);
-                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
-                          const int status_code = static_cast<int>(err->status_code);
-
-                          if ((http::is_logged_in() &&
-                               (err->matrix_error.errcode ==
-                                  mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
-                                err->matrix_error.errcode ==
-                                  mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
-                              !http::is_logged_in()) {
-                                  emit dropToLoginPageCb(msg);
-                                  return;
-                          }
-
-                          nhlog::net()->error("sync error: {} {} {} {}",
-                                              err->parse_error,
-                                              status_code,
-                                              err->error_code,
-                                              err_code);
-                          emit tryDelayedSyncCb();
-                          return;
-                  }
-
-                  emit newSyncResponse(res, since);
-          });
+    mtx::http::SyncOpts opts;
+    opts.set_presence = currentPresence();
+
+    if (!connectivityTimer_.isActive())
+        connectivityTimer_.start();
+
+    try {
+        opts.since = cache::nextBatchToken();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("failed to retrieve next batch token: {}", e.what());
+        return;
+    }
+
+    http::client()->sync(
+      opts, [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+          if (err) {
+              const auto error      = QString::fromStdString(err->matrix_error.error);
+              const auto msg        = tr("Please try to login again: %1").arg(error);
+              const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+              const int status_code = static_cast<int>(err->status_code);
+
+              if ((http::is_logged_in() &&
+                   (err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
+                    err->matrix_error.errcode == mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
+                  !http::is_logged_in()) {
+                  emit dropToLoginPageCb(msg);
+                  return;
+              }
+
+              nhlog::net()->error("sync error: {} {} {} {}",
+                                  err->parse_error,
+                                  status_code,
+                                  err->error_code,
+                                  err_code);
+              emit tryDelayedSyncCb();
+              return;
+          }
+
+          emit newSyncResponse(res, since);
+      });
 }
 
 void
 ChatPage::joinRoom(const QString &room)
 {
-        const auto room_id = room.toStdString();
-        joinRoomVia(room_id, {}, false);
+    const auto room_id = room.toStdString();
+    joinRoomVia(room_id, {}, false);
 }
 
 void
@@ -698,525 +692,722 @@ ChatPage::joinRoomVia(const std::string &room_id,
                       const std::vector<std::string> &via,
                       bool promptForConfirmation)
 {
-        if (promptForConfirmation &&
-            QMessageBox::Yes !=
-              QMessageBox::question(
-                this,
-                tr("Confirm join"),
-                tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
-                return;
-
-        http::client()->join_room(
-          room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to join room: %1")
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                          return;
-                  }
-
-                  emit tr("You joined the room");
-
-                  // We remove any invites with the same room_id.
-                  try {
-                          cache::removeInvite(room_id);
-                  } catch (const lmdb::error &e) {
-                          emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
-                  }
-
-                  view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
-          });
+    if (promptForConfirmation &&
+        QMessageBox::Yes !=
+          QMessageBox::question(
+            this,
+            tr("Confirm join"),
+            tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
+        return;
+
+    http::client()->join_room(
+      room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(
+                tr("Failed to join room: %1").arg(QString::fromStdString(err->matrix_error.error)));
+              return;
+          }
+
+          emit tr("You joined the room");
+
+          // We remove any invites with the same room_id.
+          try {
+              cache::removeInvite(room_id);
+          } catch (const lmdb::error &e) {
+              emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
+          }
+
+          view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
+      });
 }
 
 void
 ChatPage::createRoom(const mtx::requests::CreateRoom &req)
 {
-        http::client()->create_room(
-          req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
-                          const auto error      = err->matrix_error.error;
-                          const int status_code = static_cast<int>(err->status_code);
-
-                          nhlog::net()->warn(
-                            "failed to create room: {} {} ({})", error, err_code, status_code);
-
-                          emit showNotification(
-                            tr("Room creation failed: %1").arg(QString::fromStdString(error)));
-                          return;
-                  }
-
-                  QString newRoomId = QString::fromStdString(res.room_id.to_string());
-                  emit showNotification(tr("Room %1 created.").arg(newRoomId));
-                  emit newRoom(newRoomId);
-          });
+    http::client()->create_room(
+      req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
+          if (err) {
+              const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+              const auto error      = err->matrix_error.error;
+              const int status_code = static_cast<int>(err->status_code);
+
+              nhlog::net()->warn("failed to create room: {} {} ({})", error, err_code, status_code);
+
+              emit showNotification(
+                tr("Room creation failed: %1").arg(QString::fromStdString(error)));
+              return;
+          }
+
+          QString newRoomId = QString::fromStdString(res.room_id.to_string());
+          emit showNotification(tr("Room %1 created.").arg(newRoomId));
+          emit newRoom(newRoomId);
+          emit changeToRoom(newRoomId);
+      });
 }
 
 void
 ChatPage::leaveRoom(const QString &room_id)
 {
-        http::client()->leave_room(
-          room_id.toStdString(),
-          [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to leave room: %1")
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                          return;
-                  }
-
-                  emit leftRoom(room_id);
-          });
+    http::client()->leave_room(
+      room_id.toStdString(),
+      [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to leave room: %1")
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+              return;
+          }
+
+          emit leftRoom(room_id);
+      });
 }
 
 void
 ChatPage::changeRoom(const QString &room_id)
 {
-        view_manager_->rooms()->setCurrentRoom(room_id);
+    view_manager_->rooms()->setCurrentRoom(room_id);
 }
 
 void
 ChatPage::inviteUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
-
-        if (QMessageBox::question(this,
-                                  tr("Confirm invite"),
-                                  tr("Do you really want to invite %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
-
-        http::client()->invite_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to invite %1 to %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Invited user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    auto room = currentRoom();
+
+    if (QMessageBox::question(this,
+                              tr("Confirm invite"),
+                              tr("Do you really want to invite %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
+
+    http::client()->invite_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to invite %1 to %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Invited user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 void
 ChatPage::kickUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
-
-        if (QMessageBox::question(this,
-                                  tr("Confirm kick"),
-                                  tr("Do you really want to kick %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
-
-        http::client()->kick_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to kick %1 from %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Kicked user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    auto room = currentRoom();
+
+    if (QMessageBox::question(this,
+                              tr("Confirm kick"),
+                              tr("Do you really want to kick %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
+
+    http::client()->kick_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to kick %1 from %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Kicked user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 void
 ChatPage::banUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
-
-        if (QMessageBox::question(this,
-                                  tr("Confirm ban"),
-                                  tr("Do you really want to ban %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
-
-        http::client()->ban_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to ban %1 in %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Banned user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    auto room = currentRoom();
+
+    if (QMessageBox::question(this,
+                              tr("Confirm ban"),
+                              tr("Do you really want to ban %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
+
+    http::client()->ban_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to ban %1 in %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Banned user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 void
 ChatPage::unbanUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
-
-        if (QMessageBox::question(this,
-                                  tr("Confirm unban"),
-                                  tr("Do you really want to unban %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
-
-        http::client()->unban_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to unban %1 in %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Unbanned user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    auto room = currentRoom();
+
+    if (QMessageBox::question(this,
+                              tr("Confirm unban"),
+                              tr("Do you really want to unban %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
+
+    http::client()->unban_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to unban %1 in %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Unbanned user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 
 void
 ChatPage::receivedSessionKey(const std::string &room_id, const std::string &session_id)
 {
-        view_manager_->receivedSessionKey(room_id, session_id);
+    view_manager_->receivedSessionKey(room_id, session_id);
 }
 
 QString
 ChatPage::status() const
 {
-        return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString()));
+    return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString()));
 }
 
 void
 ChatPage::setStatus(const QString &status)
 {
-        http::client()->put_presence_status(
-          currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to set presence status_msg: {}",
-                                             err->matrix_error.error);
-                  }
-          });
+    http::client()->put_presence_status(
+      currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to set presence status_msg: {}", err->matrix_error.error);
+          }
+      });
 }
 
 mtx::presence::PresenceState
 ChatPage::currentPresence() const
 {
-        switch (userSettings_->presence()) {
-        case UserSettings::Presence::Online:
-                return mtx::presence::online;
-        case UserSettings::Presence::Unavailable:
-                return mtx::presence::unavailable;
-        case UserSettings::Presence::Offline:
-                return mtx::presence::offline;
-        default:
-                return mtx::presence::online;
-        }
+    switch (userSettings_->presence()) {
+    case UserSettings::Presence::Online:
+        return mtx::presence::online;
+    case UserSettings::Presence::Unavailable:
+        return mtx::presence::unavailable;
+    case UserSettings::Presence::Offline:
+        return mtx::presence::offline;
+    default:
+        return mtx::presence::online;
+    }
+}
+
+void
+ChatPage::verifyOneTimeKeyCountAfterStartup()
+{
+    http::client()->upload_keys(
+      olm::client()->create_upload_keys_request(),
+      [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
+                                    err->matrix_error.error,
+                                    static_cast<int>(err->status_code),
+                                    static_cast<int>(err->error_code));
+
+              if (err->status_code < 400 || err->status_code >= 500)
+                  return;
+          }
+
+          std::map<std::string, uint16_t> key_counts;
+          auto count = 0;
+          if (auto c = res.one_time_key_counts.find(mtx::crypto::SIGNED_CURVE25519);
+              c == res.one_time_key_counts.end()) {
+              key_counts[mtx::crypto::SIGNED_CURVE25519] = 0;
+          } else {
+              key_counts[mtx::crypto::SIGNED_CURVE25519] = c->second;
+              count                                      = c->second;
+          }
+
+          nhlog::crypto()->info(
+            "Fetched server key count {} {}", count, mtx::crypto::SIGNED_CURVE25519);
+
+          ensureOneTimeKeyCount(key_counts);
+      });
 }
 
 void
 ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
 {
-        uint16_t count = 0;
-        if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end())
-                count = c->second;
-
-        if (count < MAX_ONETIME_KEYS) {
-                const int nkeys = MAX_ONETIME_KEYS - count;
-
-                nhlog::crypto()->info(
-                  "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
-                olm::client()->generate_one_time_keys(nkeys);
-
-                http::client()->upload_keys(
-                  olm::client()->create_upload_keys_request(),
-                  [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
-                                                        err->matrix_error.error,
-                                                        static_cast<int>(err->status_code),
-                                                        static_cast<int>(err->error_code));
-
-                                  if (err->status_code < 400 || err->status_code >= 500)
-                                          return;
-                          }
-
-                          // mark as published anyway, otherwise we may end up in a loop.
-                          olm::mark_keys_as_published();
-                  });
+    if (auto count = counts.find(mtx::crypto::SIGNED_CURVE25519); count != counts.end()) {
+        nhlog::crypto()->debug(
+          "Updated server key count {} {}", count->second, mtx::crypto::SIGNED_CURVE25519);
+
+        if (count->second < MAX_ONETIME_KEYS) {
+            const int nkeys = MAX_ONETIME_KEYS - count->second;
+
+            nhlog::crypto()->info("uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
+            olm::client()->generate_one_time_keys(nkeys);
+
+            http::client()->upload_keys(
+              olm::client()->create_upload_keys_request(),
+              [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
+                  if (err) {
+                      nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
+                                            err->matrix_error.error,
+                                            static_cast<int>(err->status_code),
+                                            static_cast<int>(err->error_code));
+
+                      if (err->status_code < 400 || err->status_code >= 500)
+                          return;
+                  }
+
+                  // mark as published anyway, otherwise we may end up in a loop.
+                  olm::mark_keys_as_published();
+              });
+        } else if (count->second > 2 * MAX_ONETIME_KEYS) {
+            nhlog::crypto()->warn("too many one-time keys, deleting 1");
+            mtx::requests::ClaimKeys req;
+            req.one_time_keys[http::client()->user_id().to_string()][http::client()->device_id()] =
+              std::string(mtx::crypto::SIGNED_CURVE25519);
+            http::client()->claim_keys(
+              req, [](const mtx::responses::ClaimKeys &, mtx::http::RequestErr err) {
+                  if (err)
+                      nhlog::crypto()->warn("failed to clear 1 one-time key: {} {} {}",
+                                            err->matrix_error.error,
+                                            static_cast<int>(err->status_code),
+                                            static_cast<int>(err->error_code));
+                  else
+                      nhlog::crypto()->info("cleared 1 one-time key");
+              });
         }
+    }
 }
 
 void
 ChatPage::getProfileInfo()
 {
-        const auto userid = utils::localUser().toStdString();
+    const auto userid = utils::localUser().toStdString();
 
-        http::client()->get_profile(
-          userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to retrieve own profile info");
-                          return;
+    http::client()->get_profile(
+      userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to retrieve own profile info");
+              return;
+          }
+
+          emit setUserDisplayName(QString::fromStdString(res.display_name));
+
+          emit setUserAvatar(QString::fromStdString(res.avatar_url));
+      });
+}
+
+void
+ChatPage::getBackupVersion()
+{
+    if (!UserSettings::instance()->useOnlineKeyBackup()) {
+        nhlog::crypto()->info("Online key backup disabled.");
+        return;
+    }
+
+    http::client()->backup_version(
+      [this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("Failed to retrieve backup version");
+              if (err->status_code == 404)
+                  cache::client()->deleteBackupVersion();
+              return;
+          }
+
+          // switch to UI thread for secrets stuff
+          QTimer::singleShot(0, this, [res] {
+              auto auth_data = nlohmann::json::parse(res.auth_data);
+
+              if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
+                  auto key = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
+                  if (!key) {
+                      nhlog::crypto()->info("No key for online key backup.");
+                      cache::client()->deleteBackupVersion();
+                      return;
                   }
 
-                  emit setUserDisplayName(QString::fromStdString(res.display_name));
+                  using namespace mtx::crypto;
+                  auto pubkey = CURVE25519_public_key_from_private(to_binary_buf(base642bin(*key)));
 
-                  emit setUserAvatar(QString::fromStdString(res.avatar_url));
+                  if (auth_data["public_key"].get<std::string>() != pubkey) {
+                      nhlog::crypto()->info("Our backup key {} does not match the one "
+                                            "used in the online backup {}",
+                                            pubkey,
+                                            auth_data["public_key"]);
+                      cache::client()->deleteBackupVersion();
+                      return;
+                  }
+
+                  nhlog::crypto()->info("Using online key backup.");
+                  OnlineBackupVersion data{};
+                  data.algorithm = res.algorithm;
+                  data.version   = res.version;
+                  cache::client()->saveBackupVersion(data);
+              } else {
+                  nhlog::crypto()->info("Unsupported key backup algorithm: {}", res.algorithm);
+                  cache::client()->deleteBackupVersion();
+              }
           });
+      });
 }
 
 void
 ChatPage::initiateLogout()
 {
-        http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) {
-                if (err) {
-                        // TODO: handle special errors
-                        emit contentLoaded();
-                        nhlog::net()->warn("failed to logout: {} - {}",
-                                           mtx::errors::to_string(err->matrix_error.errcode),
-                                           err->matrix_error.error);
-                        return;
-                }
+    http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) {
+        if (err) {
+            // TODO: handle special errors
+            emit contentLoaded();
+            nhlog::net()->warn("failed to logout: {} - {}",
+                               mtx::errors::to_string(err->matrix_error.errcode),
+                               err->matrix_error.error);
+            return;
+        }
 
-                emit loggedOut();
-        });
+        emit loggedOut();
+    });
 
-        emit showOverlayProgressBar();
+    emit showOverlayProgressBar();
 }
 
 template<typename T>
 void
 ChatPage::connectCallMessage()
 {
-        connect(callManager_,
-                qOverload<const QString &, const T &>(&CallManager::newMessage),
-                view_manager_,
-                qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage));
+    connect(callManager_,
+            qOverload<const QString &, const T &>(&CallManager::newMessage),
+            view_manager_,
+            qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage));
 }
 
 void
 ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
                                    const SecretsToDecrypt &secrets)
 {
-        QString text = QInputDialog::getText(
-          ChatPage::instance(),
-          QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
-          keyDesc.name.empty()
-            ? QCoreApplication::translate(
-                "CrossSigningSecrets",
-                "Enter your recovery key or passphrase to decrypt your secrets:")
-            : QCoreApplication::translate(
-                "CrossSigningSecrets",
-                "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
-                .arg(QString::fromStdString(keyDesc.name)),
-          QLineEdit::Password);
-
-        if (text.isEmpty())
-                return;
-
-        auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
-
-        if (!decryptionKey && keyDesc.passphrase) {
-                try {
-                        decryptionKey =
-                          mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
-                } catch (std::exception &e) {
-                        nhlog::crypto()->error("Failed to derive secret key from passphrase: {}",
-                                               e.what());
-                }
-        }
-
-        if (!decryptionKey) {
-                QMessageBox::information(
-                  ChatPage::instance(),
-                  QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
-                  QCoreApplication::translate("CrossSigningSecrets",
-                                              "Failed to decrypt secrets with the "
-                                              "provided recovery key or passphrase"));
-                return;
+    QString text = QInputDialog::getText(
+      ChatPage::instance(),
+      QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
+      keyDesc.name.empty()
+        ? QCoreApplication::translate(
+            "CrossSigningSecrets", "Enter your recovery key or passphrase to decrypt your secrets:")
+        : QCoreApplication::translate(
+            "CrossSigningSecrets",
+            "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
+            .arg(QString::fromStdString(keyDesc.name)),
+      QLineEdit::Password);
+
+    if (text.isEmpty())
+        return;
+
+    auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
+
+    if (!decryptionKey && keyDesc.passphrase) {
+        try {
+            decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
+        } catch (std::exception &e) {
+            nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", e.what());
         }
+    }
 
-        for (const auto &[secretName, encryptedSecret] : secrets) {
-                auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
-                if (!decrypted.empty())
-                        cache::storeSecret(secretName, decrypted);
+    if (!decryptionKey) {
+        QMessageBox::information(
+          ChatPage::instance(),
+          QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
+          QCoreApplication::translate("CrossSigningSecrets",
+                                      "Failed to decrypt secrets with the "
+                                      "provided recovery key or passphrase"));
+        return;
+    }
+
+    auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
+    mtx::requests::KeySignaturesUpload req;
+
+    for (const auto &[secretName, encryptedSecret] : secrets) {
+        auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
+        if (!decrypted.empty()) {
+            cache::storeSecret(secretName, decrypted);
+
+            if (deviceKeys &&
+                secretName == mtx::secret_storage::secrets::cross_signing_self_signing) {
+                auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
+                if (myKey.user_id == http::client()->user_id().to_string() &&
+                    myKey.device_id == http::client()->device_id() &&
+                    myKey.keys["ed25519:" + http::client()->device_id()] ==
+                      olm::client()->identity_keys().ed25519 &&
+                    myKey.keys["curve25519:" + http::client()->device_id()] ==
+                      olm::client()->identity_keys().curve25519) {
+                    json j = myKey;
+                    j.erase("signatures");
+                    j.erase("unsigned");
+
+                    auto ssk = mtx::crypto::PkSigning::from_seed(decrypted);
+                    myKey.signatures[http::client()->user_id().to_string()]
+                                    ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
+                    req.signatures[http::client()->user_id().to_string()]
+                                  [http::client()->device_id()] = myKey;
+                }
+            } else if (deviceKeys &&
+                       secretName == mtx::secret_storage::secrets::cross_signing_master) {
+                auto mk = mtx::crypto::PkSigning::from_seed(decrypted);
+
+                if (deviceKeys->master_keys.user_id == http::client()->user_id().to_string() &&
+                    deviceKeys->master_keys.keys["ed25519:" + mk.public_key()] == mk.public_key()) {
+                    json j = deviceKeys->master_keys;
+                    j.erase("signatures");
+                    j.erase("unsigned");
+                    mtx::crypto::CrossSigningKeys master_key = j;
+                    master_key.signatures[http::client()->user_id().to_string()]
+                                         ["ed25519:" + http::client()->device_id()] =
+                      olm::client()->sign_message(j.dump());
+                    req.signatures[http::client()->user_id().to_string()][mk.public_key()] =
+                      master_key;
+                }
+            }
         }
+    }
+
+    if (!req.signatures.empty())
+        http::client()->keys_signatures_upload(
+          req, [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::net()->error("failed to upload signatures: {},{}",
+                                      mtx::errors::to_string(err->matrix_error.errcode),
+                                      static_cast<int>(err->status_code));
+              }
+
+              for (const auto &[user_id, tmp] : res.errors)
+                  for (const auto &[key_id, e] : tmp)
+                      nhlog::net()->error("signature error for user '{}' and key "
+                                          "id {}: {}, {}",
+                                          user_id,
+                                          key_id,
+                                          mtx::errors::to_string(e.errcode),
+                                          e.error);
+          });
 }
 
 void
 ChatPage::startChat(QString userid)
 {
-        auto joined_rooms = cache::joinedRooms();
-        auto room_infos   = cache::getRoomInfo(joined_rooms);
-
-        for (std::string room_id : joined_rooms) {
-                if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
-                        auto room_members = cache::roomMembers(room_id);
-                        if (std::find(room_members.begin(),
-                                      room_members.end(),
-                                      (userid).toStdString()) != room_members.end()) {
-                                view_manager_->rooms()->setCurrentRoom(
-                                  QString::fromStdString(room_id));
-                                return;
-                        }
-                }
-        }
-
-        if (QMessageBox::Yes !=
-            QMessageBox::question(
-              this,
-              tr("Confirm invite"),
-              tr("Do you really want to start a private chat with %1?").arg(userid)))
+    auto joined_rooms = cache::joinedRooms();
+    auto room_infos   = cache::getRoomInfo(joined_rooms);
+
+    for (std::string room_id : joined_rooms) {
+        if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
+            auto room_members = cache::roomMembers(room_id);
+            if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) !=
+                room_members.end()) {
+                view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
                 return;
-
-        mtx::requests::CreateRoom req;
-        req.preset     = mtx::requests::Preset::PrivateChat;
-        req.visibility = mtx::common::RoomVisibility::Private;
-        if (utils::localUser() != userid) {
-                req.invite    = {userid.toStdString()};
-                req.is_direct = true;
+            }
         }
-        emit ChatPage::instance()->createRoom(req);
+    }
+
+    if (QMessageBox::Yes !=
+        QMessageBox::question(
+          this,
+          tr("Confirm invite"),
+          tr("Do you really want to start a private chat with %1?").arg(userid)))
+        return;
+
+    mtx::requests::CreateRoom req;
+    req.preset     = mtx::requests::Preset::PrivateChat;
+    req.visibility = mtx::common::RoomVisibility::Private;
+    if (utils::localUser() != userid) {
+        req.invite    = {userid.toStdString()};
+        req.is_direct = true;
+    }
+    emit ChatPage::instance()->createRoom(req);
 }
 
 static QString
 mxidFromSegments(QStringRef sigil, QStringRef mxid)
 {
-        if (mxid.isEmpty())
-                return "";
-
-        auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
-
-        if (sigil == "u") {
-                return "@" + mxid_;
-        } else if (sigil == "roomid") {
-                return "!" + mxid_;
-        } else if (sigil == "r") {
-                return "#" + mxid_;
-                //} else if (sigil == "group") {
-                //        return "+" + mxid_;
-        } else {
-                return "";
-        }
+    if (mxid.isEmpty())
+        return "";
+
+    auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
+
+    if (sigil == "u") {
+        return "@" + mxid_;
+    } else if (sigil == "roomid") {
+        return "!" + mxid_;
+    } else if (sigil == "r") {
+        return "#" + mxid_;
+        //} else if (sigil == "group") {
+        //        return "+" + mxid_;
+    } else {
+        return "";
+    }
 }
 
-void
+bool
 ChatPage::handleMatrixUri(const QByteArray &uri)
 {
-        nhlog::ui()->info("Received uri! {}", uri.toStdString());
-        QUrl uri_{QString::fromUtf8(uri)};
+    nhlog::ui()->info("Received uri! {}", uri.toStdString());
+    QUrl uri_{QString::fromUtf8(uri)};
+
+    // Convert matrix.to URIs to proper format
+    if (uri_.scheme() == "https" && uri_.host() == "matrix.to") {
+        QString p = uri_.fragment(QUrl::FullyEncoded);
+        if (p.startsWith("/"))
+            p.remove(0, 1);
+
+        auto temp = p.split("?");
+        QString query;
+        if (temp.size() >= 2)
+            query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
+
+        temp            = temp.first().split("/");
+        auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
+        QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
+        if (!identifier.isEmpty()) {
+            if (identifier.startsWith("@")) {
+                QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                if (!query.isEmpty())
+                    newUri.append("?" + query.toUtf8());
+                return handleMatrixUri(QUrl::fromEncoded(newUri));
+            } else if (identifier.startsWith("#")) {
+                QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                if (!eventId.isEmpty())
+                    newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
+                if (!query.isEmpty())
+                    newUri.append("?" + query.toUtf8());
+                return handleMatrixUri(QUrl::fromEncoded(newUri));
+            } else if (identifier.startsWith("!")) {
+                QByteArray newUri =
+                  "matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                if (!eventId.isEmpty())
+                    newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
+                if (!query.isEmpty())
+                    newUri.append("?" + query.toUtf8());
+                return handleMatrixUri(QUrl::fromEncoded(newUri));
+            }
+        }
+    }
 
-        if (uri_.scheme() != "matrix")
-                return;
+    // non-matrix URIs are not handled by us, return false
+    if (uri_.scheme() != "matrix")
+        return false;
 
-        auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
-        if (tempPath.startsWith('/'))
-                tempPath.remove(0, 1);
-        auto segments = tempPath.splitRef('/');
+    auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
+    if (tempPath.startsWith('/'))
+        tempPath.remove(0, 1);
+    auto segments = tempPath.splitRef('/');
 
-        if (segments.size() != 2 && segments.size() != 4)
-                return;
+    if (segments.size() != 2 && segments.size() != 4)
+        return false;
 
-        auto sigil1 = segments[0];
-        auto mxid1  = mxidFromSegments(sigil1, segments[1]);
-        if (mxid1.isEmpty())
-                return;
+    auto sigil1 = segments[0];
+    auto mxid1  = mxidFromSegments(sigil1, segments[1]);
+    if (mxid1.isEmpty())
+        return false;
 
-        QString mxid2;
-        if (segments.size() == 4 && segments[2] == "e") {
-                if (segments[3].isEmpty())
-                        return;
-                else
-                        mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
-        }
+    QString mxid2;
+    if (segments.size() == 4 && segments[2] == "e") {
+        if (segments[3].isEmpty())
+            return false;
+        else
+            mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
+    }
 
-        std::vector<std::string> vias;
-        QString action;
+    std::vector<std::string> vias;
+    QString action;
 
-        for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) {
-                nhlog::ui()->info("item: {}", item.toStdString());
+    for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) {
+        nhlog::ui()->info("item: {}", item.toStdString());
 
-                if (item.startsWith("action=")) {
-                        action = item.remove("action=");
-                } else if (item.startsWith("via=")) {
-                        vias.push_back(
-                          QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
-                }
+        if (item.startsWith("action=")) {
+            action = item.remove("action=");
+        } else if (item.startsWith("via=")) {
+            vias.push_back(QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
+        }
+    }
+
+    if (sigil1 == "u") {
+        if (action.isEmpty()) {
+            auto t = view_manager_->rooms()->currentRoom();
+            if (t && cache::isRoomMember(mxid1.toStdString(), t->roomId().toStdString())) {
+                t->openUserProfile(mxid1);
+                return true;
+            }
+            emit view_manager_->openGlobalUserProfile(mxid1);
+        } else if (action == "chat") {
+            this->startChat(mxid1);
+        }
+        return true;
+    } else if (sigil1 == "roomid") {
+        auto joined_rooms = cache::joinedRooms();
+        auto targetRoomId = mxid1.toStdString();
+
+        for (auto roomid : joined_rooms) {
+            if (roomid == targetRoomId) {
+                view_manager_->rooms()->setCurrentRoom(mxid1);
+                if (!mxid2.isEmpty())
+                    view_manager_->showEvent(mxid1, mxid2);
+                return true;
+            }
         }
 
-        if (sigil1 == "u") {
-                if (action.isEmpty()) {
-                        if (auto t = view_manager_->rooms()->currentRoom())
-                                t->openUserProfile(mxid1);
-                } else if (action == "chat") {
-                        this->startChat(mxid1);
-                }
-        } else if (sigil1 == "roomid") {
-                auto joined_rooms = cache::joinedRooms();
-                auto targetRoomId = mxid1.toStdString();
-
-                for (auto roomid : joined_rooms) {
-                        if (roomid == targetRoomId) {
-                                view_manager_->rooms()->setCurrentRoom(mxid1);
-                                if (!mxid2.isEmpty())
-                                        view_manager_->showEvent(mxid1, mxid2);
-                                return;
-                        }
-                }
-
-                if (action == "join" || action.isEmpty()) {
-                        joinRoomVia(targetRoomId, vias);
-                }
-        } else if (sigil1 == "r") {
-                auto joined_rooms    = cache::joinedRooms();
-                auto targetRoomAlias = mxid1.toStdString();
-
-                for (auto roomid : joined_rooms) {
-                        auto aliases = cache::client()->getRoomAliases(roomid);
-                        if (aliases) {
-                                if (aliases->alias == targetRoomAlias) {
-                                        view_manager_->rooms()->setCurrentRoom(
-                                          QString::fromStdString(roomid));
-                                        if (!mxid2.isEmpty())
-                                                view_manager_->showEvent(
-                                                  QString::fromStdString(roomid), mxid2);
-                                        return;
-                                }
-                        }
+        if (action == "join" || action.isEmpty()) {
+            joinRoomVia(targetRoomId, vias);
+            return true;
+        }
+        return false;
+    } else if (sigil1 == "r") {
+        auto joined_rooms    = cache::joinedRooms();
+        auto targetRoomAlias = mxid1.toStdString();
+
+        for (auto roomid : joined_rooms) {
+            auto aliases = cache::client()->getRoomAliases(roomid);
+            if (aliases) {
+                if (aliases->alias == targetRoomAlias) {
+                    view_manager_->rooms()->setCurrentRoom(QString::fromStdString(roomid));
+                    if (!mxid2.isEmpty())
+                        view_manager_->showEvent(QString::fromStdString(roomid), mxid2);
+                    return true;
                 }
+            }
+        }
 
-                if (action == "join" || action.isEmpty()) {
-                        joinRoomVia(mxid1.toStdString(), vias);
-                }
+        if (action == "join" || action.isEmpty()) {
+            joinRoomVia(mxid1.toStdString(), vias);
+            return true;
         }
+        return false;
+    }
+    return false;
 }
 
-void
+bool
 ChatPage::handleMatrixUri(const QUrl &uri)
 {
-        handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
+    return handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
 }
 
 bool
 ChatPage::isRoomActive(const QString &room_id)
 {
-        return isActiveWindow() && currentRoom() == room_id;
+    return isActiveWindow() && currentRoom() == room_id;
 }
 
 QString
 ChatPage::currentRoom() const
 {
-        if (view_manager_->rooms()->currentRoom())
-                return view_manager_->rooms()->currentRoom()->roomId();
-        else
-                return "";
+    if (view_manager_->rooms()->currentRoom())
+        return view_manager_->rooms()->currentRoom()->roomId();
+    else
+        return "";
 }
diff --git a/src/ChatPage.h b/src/ChatPage.h
index c90b87f52d20736f92d0112de5238d3c8482d9b4..8f3dc53e4ff41bb8799baa812a4d9af08888e2a7 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -52,183 +52,181 @@ using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2E
 
 class ChatPage : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr);
+    ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr);
 
-        // Initialize all the components of the UI.
-        void bootstrap(QString userid, QString homeserver, QString token);
+    // Initialize all the components of the UI.
+    void bootstrap(QString userid, QString homeserver, QString token);
 
-        static ChatPage *instance() { return instance_; }
+    static ChatPage *instance() { return instance_; }
 
-        QSharedPointer<UserSettings> userSettings() { return userSettings_; }
-        CallManager *callManager() { return callManager_; }
-        TimelineViewManager *timelineManager() { return view_manager_; }
-        void deleteConfigs();
+    QSharedPointer<UserSettings> userSettings() { return userSettings_; }
+    CallManager *callManager() { return callManager_; }
+    TimelineViewManager *timelineManager() { return view_manager_; }
+    void deleteConfigs();
 
-        void initiateLogout();
+    void initiateLogout();
 
-        QString status() const;
-        void setStatus(const QString &status);
+    QString status() const;
+    void setStatus(const QString &status);
 
-        mtx::presence::PresenceState currentPresence() const;
+    mtx::presence::PresenceState currentPresence() const;
 
-        // TODO(Nico): Get rid of this!
-        QString currentRoom() const;
+    // TODO(Nico): Get rid of this!
+    QString currentRoom() const;
 
 public slots:
-        void handleMatrixUri(const QByteArray &uri);
-        void handleMatrixUri(const QUrl &uri);
-
-        void startChat(QString userid);
-        void leaveRoom(const QString &room_id);
-        void createRoom(const mtx::requests::CreateRoom &req);
-        void joinRoom(const QString &room);
-        void joinRoomVia(const std::string &room_id,
-                         const std::vector<std::string> &via,
-                         bool promptForConfirmation = true);
-
-        void inviteUser(QString userid, QString reason);
-        void kickUser(QString userid, QString reason);
-        void banUser(QString userid, QString reason);
-        void unbanUser(QString userid, QString reason);
-
-        void receivedSessionKey(const std::string &room_id, const std::string &session_id);
-        void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
-                                      const SecretsToDecrypt &secrets);
+    bool handleMatrixUri(const QByteArray &uri);
+    bool handleMatrixUri(const QUrl &uri);
+
+    void startChat(QString userid);
+    void leaveRoom(const QString &room_id);
+    void createRoom(const mtx::requests::CreateRoom &req);
+    void joinRoom(const QString &room);
+    void joinRoomVia(const std::string &room_id,
+                     const std::vector<std::string> &via,
+                     bool promptForConfirmation = true);
+
+    void inviteUser(QString userid, QString reason);
+    void kickUser(QString userid, QString reason);
+    void banUser(QString userid, QString reason);
+    void unbanUser(QString userid, QString reason);
+
+    void receivedSessionKey(const std::string &room_id, const std::string &session_id);
+    void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
+                                  const SecretsToDecrypt &secrets);
 signals:
-        void connectionLost();
-        void connectionRestored();
-
-        void notificationsRetrieved(const mtx::responses::Notifications &);
-        void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
-                                        const QPoint widgetPos);
-
-        void contentLoaded();
-        void closing();
-        void changeWindowTitle(const int);
-        void unreadMessages(int count);
-        void showNotification(const QString &msg);
-        void showLoginPage(const QString &msg);
-        void showUserSettingsPage();
-        void showOverlayProgressBar();
-
-        void ownProfileOk();
-        void setUserDisplayName(const QString &name);
-        void setUserAvatar(const QString &avatar);
-        void loggedOut();
-
-        void trySyncCb();
-        void tryDelayedSyncCb();
-        void tryInitialSyncCb();
-        void newSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token);
-        void leftRoom(const QString &room_id);
-        void newRoom(const QString &room_id);
-
-        void initializeViews(const mtx::responses::Rooms &rooms);
-        void initializeEmptyViews();
-        void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs);
-        void syncUI(const mtx::responses::Rooms &rooms);
-        void dropToLoginPageCb(const QString &msg);
-
-        void notifyMessage(const QString &roomid,
-                           const QString &eventid,
-                           const QString &roomname,
-                           const QString &sender,
-                           const QString &message,
-                           const QImage &icon);
-
-        void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
-        void themeChanged();
-        void decryptSidebarChanged();
-        void chatFocusChanged(const bool focused);
-
-        //! Signals for device verificaiton
-        void receivedDeviceVerificationAccept(
-          const mtx::events::msg::KeyVerificationAccept &message);
-        void receivedDeviceVerificationRequest(
-          const mtx::events::msg::KeyVerificationRequest &message,
-          std::string sender);
-        void receivedRoomDeviceVerificationRequest(
-          const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
-          TimelineModel *model);
-        void receivedDeviceVerificationCancel(
-          const mtx::events::msg::KeyVerificationCancel &message);
-        void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
-        void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
-        void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
-                                             std::string sender);
-        void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
-        void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
-
-        void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
-                               const SecretsToDecrypt &secrets);
+    void connectionLost();
+    void connectionRestored();
+
+    void notificationsRetrieved(const mtx::responses::Notifications &);
+    void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos);
+
+    void contentLoaded();
+    void closing();
+    void changeWindowTitle(const int);
+    void unreadMessages(int count);
+    void showNotification(const QString &msg);
+    void showLoginPage(const QString &msg);
+    void showUserSettingsPage();
+    void showOverlayProgressBar();
+
+    void ownProfileOk();
+    void setUserDisplayName(const QString &name);
+    void setUserAvatar(const QString &avatar);
+    void loggedOut();
+
+    void trySyncCb();
+    void tryDelayedSyncCb();
+    void tryInitialSyncCb();
+    void newSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token);
+    void leftRoom(const QString &room_id);
+    void newRoom(const QString &room_id);
+    void changeToRoom(const QString &room_id);
+
+    void initializeViews(const mtx::responses::Rooms &rooms);
+    void initializeEmptyViews();
+    void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs);
+    void syncUI(const mtx::responses::Rooms &rooms);
+    void dropToLoginPageCb(const QString &msg);
+
+    void notifyMessage(const QString &roomid,
+                       const QString &eventid,
+                       const QString &roomname,
+                       const QString &sender,
+                       const QString &message,
+                       const QImage &icon);
+
+    void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
+    void themeChanged();
+    void decryptSidebarChanged();
+    void chatFocusChanged(const bool focused);
+
+    //! Signals for device verificaiton
+    void receivedDeviceVerificationAccept(const mtx::events::msg::KeyVerificationAccept &message);
+    void receivedDeviceVerificationRequest(const mtx::events::msg::KeyVerificationRequest &message,
+                                           std::string sender);
+    void receivedRoomDeviceVerificationRequest(
+      const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
+      TimelineModel *model);
+    void receivedDeviceVerificationCancel(const mtx::events::msg::KeyVerificationCancel &message);
+    void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
+    void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
+    void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
+                                         std::string sender);
+    void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
+    void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
+
+    void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
+                           const SecretsToDecrypt &secrets);
 
 private slots:
-        void logout();
-        void removeRoom(const QString &room_id);
-        void changeRoom(const QString &room_id);
-        void dropToLoginPage(const QString &msg);
+    void logout();
+    void removeRoom(const QString &room_id);
+    void changeRoom(const QString &room_id);
+    void dropToLoginPage(const QString &msg);
 
-        void handleSyncResponse(const mtx::responses::Sync &res,
-                                const std::string &prev_batch_token);
+    void handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token);
 
 private:
-        static ChatPage *instance_;
+    static ChatPage *instance_;
 
-        void startInitialSync();
-        void tryInitialSync();
-        void trySync();
-        void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
-        void getProfileInfo();
+    void startInitialSync();
+    void tryInitialSync();
+    void trySync();
+    void verifyOneTimeKeyCountAfterStartup();
+    void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
+    void getProfileInfo();
+    void getBackupVersion();
 
-        //! Check if the given room is currently open.
-        bool isRoomActive(const QString &room_id);
+    //! Check if the given room is currently open.
+    bool isRoomActive(const QString &room_id);
 
-        using UserID      = QString;
-        using Membership  = mtx::events::StateEvent<mtx::events::state::Member>;
-        using Memberships = std::map<std::string, Membership>;
+    using UserID      = QString;
+    using Membership  = mtx::events::StateEvent<mtx::events::state::Member>;
+    using Memberships = std::map<std::string, Membership>;
 
-        void loadStateFromCache();
-        void resetUI();
+    void loadStateFromCache();
+    void resetUI();
 
-        template<class Collection>
-        Memberships getMemberships(const std::vector<Collection> &events) const;
+    template<class Collection>
+    Memberships getMemberships(const std::vector<Collection> &events) const;
 
-        //! Send desktop notification for the received messages.
-        void sendNotifications(const mtx::responses::Notifications &);
+    //! Send desktop notification for the received messages.
+    void sendNotifications(const mtx::responses::Notifications &);
 
-        template<typename T>
-        void connectCallMessage();
+    template<typename T>
+    void connectCallMessage();
 
-        QHBoxLayout *topLayout_;
+    QHBoxLayout *topLayout_;
 
-        TimelineViewManager *view_manager_;
+    TimelineViewManager *view_manager_;
 
-        QTimer connectivityTimer_;
-        std::atomic_bool isConnected_;
+    QTimer connectivityTimer_;
+    std::atomic_bool isConnected_;
 
-        // Global user settings.
-        QSharedPointer<UserSettings> userSettings_;
+    // Global user settings.
+    QSharedPointer<UserSettings> userSettings_;
 
-        NotificationsManager notificationsManager;
-        CallManager *callManager_;
+    NotificationsManager notificationsManager;
+    CallManager *callManager_;
 };
 
 template<class Collection>
 std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>>
 ChatPage::getMemberships(const std::vector<Collection> &collection) const
 {
-        std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> memberships;
+    std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> memberships;
 
-        using Member = mtx::events::StateEvent<mtx::events::state::Member>;
+    using Member = mtx::events::StateEvent<mtx::events::state::Member>;
 
-        for (const auto &event : collection) {
-                if (auto member = std::get_if<Member>(event)) {
-                        memberships.emplace(member->state_key, *member);
-                }
+    for (const auto &event : collection) {
+        if (auto member = std::get_if<Member>(event)) {
+            memberships.emplace(member->state_key, *member);
         }
+    }
 
-        return memberships;
+    return memberships;
 }
diff --git a/src/Clipboard.cpp b/src/Clipboard.cpp
index d4d5bab78a3deb8b48628a2ea705835c2208e5c5..93d913bede3bc6ee50503709771989580af872bc 100644
--- a/src/Clipboard.cpp
+++ b/src/Clipboard.cpp
@@ -10,18 +10,17 @@
 Clipboard::Clipboard(QObject *parent)
   : QObject(parent)
 {
-        connect(
-          QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged);
+    connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged);
 }
 
 void
 Clipboard::setText(QString text)
 {
-        QGuiApplication::clipboard()->setText(text);
+    QGuiApplication::clipboard()->setText(text);
 }
 
 QString
 Clipboard::text() const
 {
-        return QGuiApplication::clipboard()->text();
+    return QGuiApplication::clipboard()->text();
 }
diff --git a/src/Clipboard.h b/src/Clipboard.h
index fa74da22e6ae1e8cbc840ef14c5136080f7d157f..1a6584cadfcdf183c32e3d0154e1406409d5e477 100644
--- a/src/Clipboard.h
+++ b/src/Clipboard.h
@@ -9,14 +9,14 @@
 
 class Clipboard : public QObject
 {
-        Q_OBJECT
-        Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
+    Q_OBJECT
+    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
 
 public:
-        Clipboard(QObject *parent = nullptr);
+    Clipboard(QObject *parent = nullptr);
 
-        QString text() const;
-        void setText(QString text_);
+    QString text() const;
+    void setText(QString text_);
 signals:
-        void textChanged();
+    void textChanged();
 };
diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp
index 41fd5d8f4332233a91612342adf9293c02eccdb2..9c371c8c260f83f8f7ae214a8b420ac7baa9498f 100644
--- a/src/ColorImageProvider.cpp
+++ b/src/ColorImageProvider.cpp
@@ -9,23 +9,23 @@
 QPixmap
 ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &)
 {
-        auto args = id.split('?');
+    auto args = id.split('?');
 
-        QPixmap source(args[0]);
+    QPixmap source(args[0]);
 
-        if (size)
-                *size = QSize(source.width(), source.height());
+    if (size)
+        *size = QSize(source.width(), source.height());
 
-        if (args.size() < 2)
-                return source;
+    if (args.size() < 2)
+        return source;
 
-        QColor color(args[1]);
+    QColor color(args[1]);
 
-        QPixmap colorized = source;
-        QPainter painter(&colorized);
-        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
-        painter.fillRect(colorized.rect(), color);
-        painter.end();
+    QPixmap colorized = source;
+    QPainter painter(&colorized);
+    painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+    painter.fillRect(colorized.rect(), color);
+    painter.end();
 
-        return colorized;
+    return colorized;
 }
diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h
index 9ae8c85e397d4922669438dc5146cf2af93cb7c3..f2997e0afb037f9307f06e4a94a4d40c85979008 100644
--- a/src/ColorImageProvider.h
+++ b/src/ColorImageProvider.h
@@ -7,9 +7,9 @@
 class ColorImageProvider : public QQuickImageProvider
 {
 public:
-        ColorImageProvider()
-          : QQuickImageProvider(QQuickImageProvider::Pixmap)
-        {}
+    ColorImageProvider()
+      : QQuickImageProvider(QQuickImageProvider::Pixmap)
+    {}
 
-        QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
+    QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
 };
diff --git a/src/CombinedImagePackModel.cpp b/src/CombinedImagePackModel.cpp
index 341a34ec5e6e78188d0e2e190523bb75f0c62f4e..9a52f8103ca1c75835944038b50dd43a26320a3f 100644
--- a/src/CombinedImagePackModel.cpp
+++ b/src/CombinedImagePackModel.cpp
@@ -13,65 +13,65 @@ CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId,
   : QAbstractListModel(parent)
   , room_id(roomId)
 {
-        auto packs = cache::client()->getImagePacks(room_id, stickers);
+    auto packs = cache::client()->getImagePacks(room_id, stickers);
 
-        for (const auto &pack : packs) {
-                QString packname =
-                  pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : "";
+    for (const auto &pack : packs) {
+        QString packname =
+          pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : "";
 
-                for (const auto &img : pack.pack.images) {
-                        ImageDesc i{};
-                        i.shortcode = QString::fromStdString(img.first);
-                        i.packname  = packname;
-                        i.image     = img.second;
-                        images.push_back(std::move(i));
-                }
+        for (const auto &img : pack.pack.images) {
+            ImageDesc i{};
+            i.shortcode = QString::fromStdString(img.first);
+            i.packname  = packname;
+            i.image     = img.second;
+            images.push_back(std::move(i));
         }
+    }
 }
 
 int
 CombinedImagePackModel::rowCount(const QModelIndex &) const
 {
-        return (int)images.size();
+    return (int)images.size();
 }
 
 QHash<int, QByteArray>
 CombinedImagePackModel::roleNames() const
 {
-        return {
-          {CompletionModel::CompletionRole, "completionRole"},
-          {CompletionModel::SearchRole, "searchRole"},
-          {CompletionModel::SearchRole2, "searchRole2"},
-          {Roles::Url, "url"},
-          {Roles::ShortCode, "shortcode"},
-          {Roles::Body, "body"},
-          {Roles::PackName, "packname"},
-          {Roles::OriginalRow, "originalRow"},
-        };
+    return {
+      {CompletionModel::CompletionRole, "completionRole"},
+      {CompletionModel::SearchRole, "searchRole"},
+      {CompletionModel::SearchRole2, "searchRole2"},
+      {Roles::Url, "url"},
+      {Roles::ShortCode, "shortcode"},
+      {Roles::Body, "body"},
+      {Roles::PackName, "packname"},
+      {Roles::OriginalRow, "originalRow"},
+    };
 }
 
 QVariant
 CombinedImagePackModel::data(const QModelIndex &index, int role) const
 {
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                switch (role) {
-                case CompletionModel::CompletionRole:
-                        return QString::fromStdString(images[index.row()].image.url);
-                case Roles::Url:
-                        return QString::fromStdString(images[index.row()].image.url);
-                case CompletionModel::SearchRole:
-                case Roles::ShortCode:
-                        return images[index.row()].shortcode;
-                case CompletionModel::SearchRole2:
-                case Roles::Body:
-                        return QString::fromStdString(images[index.row()].image.body);
-                case Roles::PackName:
-                        return images[index.row()].packname;
-                case Roles::OriginalRow:
-                        return index.row();
-                default:
-                        return {};
-                }
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        switch (role) {
+        case CompletionModel::CompletionRole:
+            return QString::fromStdString(images[index.row()].image.url);
+        case Roles::Url:
+            return QString::fromStdString(images[index.row()].image.url);
+        case CompletionModel::SearchRole:
+        case Roles::ShortCode:
+            return images[index.row()].shortcode;
+        case CompletionModel::SearchRole2:
+        case Roles::Body:
+            return QString::fromStdString(images[index.row()].image.body);
+        case Roles::PackName:
+            return images[index.row()].packname;
+        case Roles::OriginalRow:
+            return index.row();
+        default:
+            return {};
         }
-        return {};
+    }
+    return {};
 }
diff --git a/src/CombinedImagePackModel.h b/src/CombinedImagePackModel.h
index f0f697999b8617a5fff08d8597f515406ee6324d..ec49b3259e53f546c6a2ae1dcdb0e3c6258e76d3 100644
--- a/src/CombinedImagePackModel.h
+++ b/src/CombinedImagePackModel.h
@@ -10,39 +10,39 @@
 
 class CombinedImagePackModel : public QAbstractListModel
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        enum Roles
-        {
-                Url = Qt::UserRole,
-                ShortCode,
-                Body,
-                PackName,
-                OriginalRow,
-        };
-
-        CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-        QVariant data(const QModelIndex &index, int role) const override;
-
-        mtx::events::msc2545::PackImage imageAt(int row)
-        {
-                if (row < 0 || static_cast<size_t>(row) >= images.size())
-                        return {};
-                return images.at(static_cast<size_t>(row)).image;
-        }
+    enum Roles
+    {
+        Url = Qt::UserRole,
+        ShortCode,
+        Body,
+        PackName,
+        OriginalRow,
+    };
+
+    CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+    QVariant data(const QModelIndex &index, int role) const override;
+
+    mtx::events::msc2545::PackImage imageAt(int row)
+    {
+        if (row < 0 || static_cast<size_t>(row) >= images.size())
+            return {};
+        return images.at(static_cast<size_t>(row)).image;
+    }
 
 private:
-        std::string room_id;
+    std::string room_id;
 
-        struct ImageDesc
-        {
-                QString shortcode;
-                QString packname;
+    struct ImageDesc
+    {
+        QString shortcode;
+        QString packname;
 
-                mtx::events::msc2545::PackImage image;
-        };
+        mtx::events::msc2545::PackImage image;
+    };
 
-        std::vector<ImageDesc> images;
+    std::vector<ImageDesc> images;
 };
diff --git a/src/CompletionModelRoles.h b/src/CompletionModelRoles.h
index 8505e76185dbd04404d28f1a2bdc33b920757666..9a735d6053cd6aeb93dd7135b1fcd2bc46df619c 100644
--- a/src/CompletionModelRoles.h
+++ b/src/CompletionModelRoles.h
@@ -12,8 +12,8 @@ namespace CompletionModel {
 // Start at Qt::UserRole * 2 to prevent clashes
 enum Roles
 {
-        CompletionRole = Qt::UserRole * 2, // The string to replace the active completion
-        SearchRole,                        // String completer uses for search
-        SearchRole2,                       // Secondary string completer uses for search
+    CompletionRole = Qt::UserRole * 2, // The string to replace the active completion
+    SearchRole,                        // String completer uses for search
+    SearchRole2,                       // Secondary string completer uses for search
 };
 }
diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp
index e68944c7e283a9375a8ee5b1e54d7b200f22456b..454f54b744868c3caebf7b41e93d458754004181 100644
--- a/src/CompletionProxyModel.cpp
+++ b/src/CompletionProxyModel.cpp
@@ -18,154 +18,154 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
   , maxMistakes_(max_mistakes)
   , max_completions_(max_completions)
 {
-        setSourceModel(model);
-        QChar splitPoints(' ');
-
-        // insert all the full texts
-        for (int i = 0; i < sourceModel()->rowCount(); i++) {
-                if (static_cast<size_t>(i) < max_completions_)
-                        mapping.push_back(i);
-
-                auto string1 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
-                                 .toString()
-                                 .toLower();
-                if (!string1.isEmpty())
-                        trie_.insert(string1.toUcs4(), i);
-
-                auto string2 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
-                                 .toString()
-                                 .toLower();
-                if (!string2.isEmpty())
-                        trie_.insert(string2.toUcs4(), i);
+    setSourceModel(model);
+    QChar splitPoints(' ');
+
+    // insert all the full texts
+    for (int i = 0; i < sourceModel()->rowCount(); i++) {
+        if (static_cast<size_t>(i) < max_completions_)
+            mapping.push_back(i);
+
+        auto string1 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
+                         .toString()
+                         .toLower();
+        if (!string1.isEmpty())
+            trie_.insert(string1.toUcs4(), i);
+
+        auto string2 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
+                         .toString()
+                         .toLower();
+        if (!string2.isEmpty())
+            trie_.insert(string2.toUcs4(), i);
+    }
+
+    // insert the partial matches
+    for (int i = 0; i < sourceModel()->rowCount(); i++) {
+        auto string1 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
+                         .toString()
+                         .toLower();
+
+        for (const auto &e : string1.splitRef(splitPoints)) {
+            if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
+                trie_.insert(e.toUcs4(), i);
         }
 
-        // insert the partial matches
-        for (int i = 0; i < sourceModel()->rowCount(); i++) {
-                auto string1 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
-                                 .toString()
-                                 .toLower();
-
-                for (const auto &e : string1.splitRef(splitPoints)) {
-                        if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
-                                trie_.insert(e.toUcs4(), i);
-                }
-
-                auto string2 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
-                                 .toString()
-                                 .toLower();
-
-                if (!string2.isEmpty()) {
-                        for (const auto &e : string2.splitRef(splitPoints)) {
-                                if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
-                                        trie_.insert(e.toUcs4(), i);
-                        }
-                }
-        }
+        auto string2 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
+                         .toString()
+                         .toLower();
 
-        connect(
-          this,
-          &CompletionProxyModel::newSearchString,
-          this,
-          [this](QString s) {
-                  s.remove(":");
-                  s.remove("@");
-                  searchString_ = s.toLower();
-                  invalidate();
-          },
-          Qt::QueuedConnection);
+        if (!string2.isEmpty()) {
+            for (const auto &e : string2.splitRef(splitPoints)) {
+                if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
+                    trie_.insert(e.toUcs4(), i);
+            }
+        }
+    }
+
+    connect(
+      this,
+      &CompletionProxyModel::newSearchString,
+      this,
+      [this](QString s) {
+          s.remove(":");
+          s.remove("@");
+          searchString_ = s.toLower();
+          invalidate();
+      },
+      Qt::QueuedConnection);
 }
 
 void
 CompletionProxyModel::invalidate()
 {
-        auto key = searchString_.toUcs4();
-        beginResetModel();
-        if (!key.empty()) // return default model data, if no search string
-                mapping = trie_.search(key, max_completions_, maxMistakes_);
-        endResetModel();
+    auto key = searchString_.toUcs4();
+    beginResetModel();
+    if (!key.empty()) // return default model data, if no search string
+        mapping = trie_.search(key, max_completions_, maxMistakes_);
+    endResetModel();
 }
 
 QHash<int, QByteArray>
 CompletionProxyModel::roleNames() const
 {
-        return this->sourceModel()->roleNames();
+    return this->sourceModel()->roleNames();
 }
 
 int
 CompletionProxyModel::rowCount(const QModelIndex &) const
 {
-        if (searchString_.isEmpty())
-                return std::min(static_cast<int>(std::min<size_t>(max_completions_,
-                                                                  std::numeric_limits<int>::max())),
-                                sourceModel()->rowCount());
-        else
-                return (int)mapping.size();
+    if (searchString_.isEmpty())
+        return std::min(
+          static_cast<int>(std::min<size_t>(max_completions_, std::numeric_limits<int>::max())),
+          sourceModel()->rowCount());
+    else
+        return (int)mapping.size();
 }
 
 QModelIndex
 CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
 {
-        // return default model data, if no search string
-        if (searchString_.isEmpty()) {
-                return index(sourceIndex.row(), 0);
-        }
-
-        for (int i = 0; i < (int)mapping.size(); i++) {
-                if (mapping[i] == sourceIndex.row()) {
-                        return index(i, 0);
-                }
+    // return default model data, if no search string
+    if (searchString_.isEmpty()) {
+        return index(sourceIndex.row(), 0);
+    }
+
+    for (int i = 0; i < (int)mapping.size(); i++) {
+        if (mapping[i] == sourceIndex.row()) {
+            return index(i, 0);
         }
-        return QModelIndex();
+    }
+    return QModelIndex();
 }
 
 QModelIndex
 CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const
 {
-        auto row = proxyIndex.row();
+    auto row = proxyIndex.row();
 
-        // return default model data, if no search string
-        if (searchString_.isEmpty()) {
-                return index(row, 0);
-        }
+    // return default model data, if no search string
+    if (searchString_.isEmpty()) {
+        return index(row, 0);
+    }
 
-        if (row < 0 || row >= (int)mapping.size())
-                return QModelIndex();
+    if (row < 0 || row >= (int)mapping.size())
+        return QModelIndex();
 
-        return sourceModel()->index(mapping[row], 0);
+    return sourceModel()->index(mapping[row], 0);
 }
 
 QModelIndex
 CompletionProxyModel::index(int row, int column, const QModelIndex &) const
 {
-        return createIndex(row, column);
+    return createIndex(row, column);
 }
 
 QModelIndex
 CompletionProxyModel::parent(const QModelIndex &) const
 {
-        return QModelIndex{};
+    return QModelIndex{};
 }
 int
 CompletionProxyModel::columnCount(const QModelIndex &) const
 {
-        return sourceModel()->columnCount();
+    return sourceModel()->columnCount();
 }
 
 QVariant
 CompletionProxyModel::completionAt(int i) const
 {
-        if (i >= 0 && i < rowCount())
-                return data(index(i, 0), CompletionModel::CompletionRole);
-        else
-                return {};
+    if (i >= 0 && i < rowCount())
+        return data(index(i, 0), CompletionModel::CompletionRole);
+    else
+        return {};
 }
 
 void
 CompletionProxyModel::setSearchString(QString s)
 {
-        emit newSearchString(s);
+    emit newSearchString(s);
 }
diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h
index d85d93437bd4ff5188253f54e0b44c2028b3fa0d..c6331a2d6f084d0bc4fc3eee82f6543332bca4a4 100644
--- a/src/CompletionProxyModel.h
+++ b/src/CompletionProxyModel.h
@@ -11,179 +11,176 @@
 template<typename Key, typename Value>
 struct trie
 {
-        std::vector<Value> values;
-        std::map<Key, trie> next;
-
-        void insert(const QVector<Key> &keys, const Value &v)
-        {
-                auto t = this;
-                for (const auto k : keys) {
-                        t = &t->next[k];
-                }
-
-                t->values.push_back(v);
+    std::vector<Value> values;
+    std::map<Key, trie> next;
+
+    void insert(const QVector<Key> &keys, const Value &v)
+    {
+        auto t = this;
+        for (const auto k : keys) {
+            t = &t->next[k];
         }
 
-        std::vector<Value> valuesAndSubvalues(size_t limit = -1) const
-        {
-                std::vector<Value> ret;
-                if (limit < 200)
-                        ret.reserve(limit);
-
-                for (const auto &v : values) {
-                        if (ret.size() >= limit)
-                                return ret;
-                        else
-                                ret.push_back(v);
-                }
+        t->values.push_back(v);
+    }
 
-                for (const auto &[k, t] : next) {
-                        (void)k;
-                        if (ret.size() >= limit)
-                                return ret;
-                        else {
-                                auto temp = t.valuesAndSubvalues(limit - ret.size());
-                                for (auto &&v : temp) {
-                                        if (ret.size() >= limit)
-                                                return ret;
-
-                                        if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
-                                                ret.push_back(std::move(v));
-                                        }
-                                }
-                        }
-                }
+    std::vector<Value> valuesAndSubvalues(size_t limit = -1) const
+    {
+        std::vector<Value> ret;
+        if (limit < 200)
+            ret.reserve(limit);
 
+        for (const auto &v : values) {
+            if (ret.size() >= limit)
                 return ret;
+            else
+                ret.push_back(v);
         }
 
-        std::vector<Value> search(const QVector<Key> &keys, //< TODO(Nico): replace this with a span
-                                  size_t result_count_limit,
-                                  size_t max_edit_distance_ = 2) const
-        {
-                std::vector<Value> ret;
-                if (!result_count_limit)
+        for (const auto &[k, t] : next) {
+            (void)k;
+            if (ret.size() >= limit)
+                return ret;
+            else {
+                auto temp = t.valuesAndSubvalues(limit - ret.size());
+                for (auto &&v : temp) {
+                    if (ret.size() >= limit)
                         return ret;
 
-                if (keys.isEmpty())
-                        return valuesAndSubvalues(result_count_limit);
+                    if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
+                        ret.push_back(std::move(v));
+                    }
+                }
+            }
+        }
 
-                auto append = [&ret, result_count_limit](std::vector<Value> &&in) {
-                        for (auto &&v : in) {
-                                if (ret.size() >= result_count_limit)
-                                        return;
+        return ret;
+    }
 
-                                if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
-                                        ret.push_back(std::move(v));
-                                }
-                        }
-                };
-
-                auto limit = [&ret, result_count_limit] {
-                        return std::min(result_count_limit, (result_count_limit - ret.size()) * 2);
-                };
-
-                // Try first exact matches, then with maximum errors
-                for (size_t max_edit_distance = 0;
-                     max_edit_distance <= max_edit_distance_ && ret.size() < result_count_limit;
-                     max_edit_distance += 1) {
-                        if (max_edit_distance && ret.size() < result_count_limit) {
-                                max_edit_distance -= 1;
-
-                                // swap chars case
-                                if (keys.size() >= 2) {
-                                        auto t = this;
-                                        for (int i = 1; i >= 0; i--) {
-                                                if (auto e = t->next.find(keys[i]);
-                                                    e != t->next.end()) {
-                                                        t = &e->second;
-                                                } else {
-                                                        t = nullptr;
-                                                        break;
-                                                }
-                                        }
-
-                                        if (t) {
-                                                append(t->search(
-                                                  keys.mid(2), limit(), max_edit_distance));
-                                        }
-                                }
-
-                                // insert case
-                                for (const auto &[k, t] : this->next) {
-                                        if (k == keys[0])
-                                                continue;
-                                        if (ret.size() >= limit())
-                                                break;
-
-                                        // insert
-                                        append(t.search(keys, limit(), max_edit_distance));
-                                }
-
-                                // delete character case
-                                append(this->search(keys.mid(1), limit(), max_edit_distance));
-
-                                // substitute case
-                                for (const auto &[k, t] : this->next) {
-                                        if (k == keys[0])
-                                                continue;
-                                        if (ret.size() >= limit())
-                                                break;
-
-                                        // substitute
-                                        append(t.search(keys.mid(1), limit(), max_edit_distance));
-                                }
-
-                                max_edit_distance += 1;
-                        }
+    std::vector<Value> search(const QVector<Key> &keys, //< TODO(Nico): replace this with a span
+                              size_t result_count_limit,
+                              size_t max_edit_distance_ = 2) const
+    {
+        std::vector<Value> ret;
+        if (!result_count_limit)
+            return ret;
+
+        if (keys.isEmpty())
+            return valuesAndSubvalues(result_count_limit);
+
+        auto append = [&ret, result_count_limit](std::vector<Value> &&in) {
+            for (auto &&v : in) {
+                if (ret.size() >= result_count_limit)
+                    return;
 
-                        if (auto e = this->next.find(keys[0]); e != this->next.end()) {
-                                append(e->second.search(keys.mid(1), limit(), max_edit_distance));
+                if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
+                    ret.push_back(std::move(v));
+                }
+            }
+        };
+
+        auto limit = [&ret, result_count_limit] {
+            return std::min(result_count_limit, (result_count_limit - ret.size()) * 2);
+        };
+
+        // Try first exact matches, then with maximum errors
+        for (size_t max_edit_distance = 0;
+             max_edit_distance <= max_edit_distance_ && ret.size() < result_count_limit;
+             max_edit_distance += 1) {
+            if (max_edit_distance && ret.size() < result_count_limit) {
+                max_edit_distance -= 1;
+
+                // swap chars case
+                if (keys.size() >= 2) {
+                    auto t = this;
+                    for (int i = 1; i >= 0; i--) {
+                        if (auto e = t->next.find(keys[i]); e != t->next.end()) {
+                            t = &e->second;
+                        } else {
+                            t = nullptr;
+                            break;
                         }
+                    }
+
+                    if (t) {
+                        append(t->search(keys.mid(2), limit(), max_edit_distance));
+                    }
                 }
 
-                return ret;
+                // insert case
+                for (const auto &[k, t] : this->next) {
+                    if (k == keys[0])
+                        continue;
+                    if (ret.size() >= limit())
+                        break;
+
+                    // insert
+                    append(t.search(keys, limit(), max_edit_distance));
+                }
+
+                // delete character case
+                append(this->search(keys.mid(1), limit(), max_edit_distance));
+
+                // substitute case
+                for (const auto &[k, t] : this->next) {
+                    if (k == keys[0])
+                        continue;
+                    if (ret.size() >= limit())
+                        break;
+
+                    // substitute
+                    append(t.search(keys.mid(1), limit(), max_edit_distance));
+                }
+
+                max_edit_distance += 1;
+            }
+
+            if (auto e = this->next.find(keys[0]); e != this->next.end()) {
+                append(e->second.search(keys.mid(1), limit(), max_edit_distance));
+            }
         }
+
+        return ret;
+    }
 };
 
 class CompletionProxyModel : public QAbstractProxyModel
 {
-        Q_OBJECT
-        Q_PROPERTY(
-          QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
+    Q_OBJECT
+    Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
 public:
-        CompletionProxyModel(QAbstractItemModel *model,
-                             int max_mistakes       = 2,
-                             size_t max_completions = 7,
-                             QObject *parent        = nullptr);
+    CompletionProxyModel(QAbstractItemModel *model,
+                         int max_mistakes       = 2,
+                         size_t max_completions = 7,
+                         QObject *parent        = nullptr);
 
-        void invalidate();
+    void invalidate();
 
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-        int columnCount(const QModelIndex &) const override;
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+    int columnCount(const QModelIndex &) const override;
 
-        QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
-        QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
+    QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
+    QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
 
-        QModelIndex index(int row,
-                          int column,
-                          const QModelIndex &parent = QModelIndex()) const override;
-        QModelIndex parent(const QModelIndex &) const override;
+    QModelIndex index(int row,
+                      int column,
+                      const QModelIndex &parent = QModelIndex()) const override;
+    QModelIndex parent(const QModelIndex &) const override;
 
 public slots:
-        QVariant completionAt(int i) const;
+    QVariant completionAt(int i) const;
 
-        void setSearchString(QString s);
-        QString searchString() const { return searchString_; }
+    void setSearchString(QString s);
+    QString searchString() const { return searchString_; }
 
 signals:
-        void newSearchString(QString);
+    void newSearchString(QString);
 
 private:
-        QString searchString_;
-        trie<uint, int> trie_;
-        std::vector<int> mapping;
-        int maxMistakes_;
-        size_t max_completions_;
+    QString searchString_;
+    trie<uint, int> trie_;
+    std::vector<int> mapping;
+    int maxMistakes_;
+    size_t max_completions_;
 };
diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp
deleted file mode 100644
index 1760ea9a9d86896a2422f60bbb2eb31130f8b1f5..0000000000000000000000000000000000000000
--- a/src/DeviceVerificationFlow.cpp
+++ /dev/null
@@ -1,882 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "DeviceVerificationFlow.h"
-
-#include "Cache.h"
-#include "Cache_p.h"
-#include "ChatPage.h"
-#include "Logging.h"
-#include "Utils.h"
-#include "timeline/TimelineModel.h"
-
-#include <QDateTime>
-#include <QTimer>
-#include <iostream>
-
-static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
-
-namespace msgs = mtx::events::msg;
-
-static mtx::events::msg::KeyVerificationMac
-key_verification_mac(mtx::crypto::SAS *sas,
-                     mtx::identifiers::User sender,
-                     const std::string &senderDevice,
-                     mtx::identifiers::User receiver,
-                     const std::string &receiverDevice,
-                     const std::string &transactionId,
-                     std::map<std::string, std::string> keys);
-
-DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
-                                               DeviceVerificationFlow::Type flow_type,
-                                               TimelineModel *model,
-                                               QString userID,
-                                               QString deviceId_)
-  : sender(false)
-  , type(flow_type)
-  , deviceId(deviceId_)
-  , model_(model)
-{
-        timeout = new QTimer(this);
-        timeout->setSingleShot(true);
-        this->sas           = olm::client()->sas_init();
-        this->isMacVerified = false;
-
-        auto user_id   = userID.toStdString();
-        this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id);
-        cache::client()->query_keys(
-          user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to query device keys: {},{}",
-                                             mtx::errors::to_string(err->matrix_error.errcode),
-                                             static_cast<int>(err->status_code));
-                          return;
-                  }
-
-                  if (!this->deviceId.isEmpty() &&
-                      (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
-                          nhlog::net()->warn("no devices retrieved {}", user_id);
-                          return;
-                  }
-
-                  this->their_keys = res;
-          });
-
-        cache::client()->query_keys(
-          http::client()->user_id().to_string(),
-          [this](const UserKeyCache &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to query device keys: {},{}",
-                                             mtx::errors::to_string(err->matrix_error.errcode),
-                                             static_cast<int>(err->status_code));
-                          return;
-                  }
-
-                  if (res.master_keys.keys.empty())
-                          return;
-
-                  if (auto status =
-                        cache::verificationStatus(http::client()->user_id().to_string());
-                      status && status->user_verified == crypto::Trust::Verified)
-                          this->our_trusted_master_key = res.master_keys.keys.begin()->second;
-          });
-
-        if (model) {
-                connect(this->model_,
-                        &TimelineModel::updateFlowEventId,
-                        this,
-                        [this](std::string event_id_) {
-                                this->relation.rel_type = mtx::common::RelationType::Reference;
-                                this->relation.event_id = event_id_;
-                                this->transaction_id    = event_id_;
-                        });
-        }
-
-        connect(timeout, &QTimer::timeout, this, [this]() {
-                nhlog::crypto()->info("verification: timeout");
-                if (state_ != Success && state_ != Failed)
-                        this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
-        });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationStart,
-                this,
-                &DeviceVerificationFlow::handleStartMessage);
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationAccept,
-                this,
-                [this](const mtx::events::msg::KeyVerificationAccept &msg) {
-                        nhlog::crypto()->info("verification: received accept");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-                        if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
-                            (msg.hash == "sha256") &&
-                            (msg.message_authentication_code == "hkdf-hmac-sha256")) {
-                                this->commitment = msg.commitment;
-                                if (std::find(msg.short_authentication_string.begin(),
-                                              msg.short_authentication_string.end(),
-                                              mtx::events::msg::SASMethods::Emoji) !=
-                                    msg.short_authentication_string.end()) {
-                                        this->method = mtx::events::msg::SASMethods::Emoji;
-                                } else {
-                                        this->method = mtx::events::msg::SASMethods::Decimal;
-                                }
-                                this->mac_method = msg.message_authentication_code;
-                                this->sendVerificationKey();
-                        } else {
-                                this->cancelVerification(
-                                  DeviceVerificationFlow::Error::UnknownMethod);
-                        }
-                });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationCancel,
-                this,
-                [this](const mtx::events::msg::KeyVerificationCancel &msg) {
-                        nhlog::crypto()->info("verification: received cancel");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-                        error_ = User;
-                        emit errorChanged();
-                        setState(Failed);
-                });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationKey,
-                this,
-                [this](const mtx::events::msg::KeyVerificationKey &msg) {
-                        nhlog::crypto()->info("verification: received key");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-
-                        if (sender) {
-                                if (state_ != WaitingForOtherToAccept) {
-                                        this->cancelVerification(OutOfOrder);
-                                        return;
-                                }
-                        } else {
-                                if (state_ != WaitingForKeys) {
-                                        this->cancelVerification(OutOfOrder);
-                                        return;
-                                }
-                        }
-
-                        this->sas->set_their_key(msg.key);
-                        std::string info;
-                        if (this->sender == true) {
-                                info = "MATRIX_KEY_VERIFICATION_SAS|" +
-                                       http::client()->user_id().to_string() + "|" +
-                                       http::client()->device_id() + "|" + this->sas->public_key() +
-                                       "|" + this->toClient.to_string() + "|" +
-                                       this->deviceId.toStdString() + "|" + msg.key + "|" +
-                                       this->transaction_id;
-                        } else {
-                                info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
-                                       "|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
-                                       http::client()->user_id().to_string() + "|" +
-                                       http::client()->device_id() + "|" + this->sas->public_key() +
-                                       "|" + this->transaction_id;
-                        }
-
-                        nhlog::ui()->info("Info is: '{}'", info);
-
-                        if (this->sender == false) {
-                                this->sendVerificationKey();
-                        } else {
-                                if (this->commitment !=
-                                    mtx::crypto::bin2base64_unpadded(
-                                      mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
-                                        this->cancelVerification(
-                                          DeviceVerificationFlow::Error::MismatchedCommitment);
-                                        return;
-                                }
-                        }
-
-                        if (this->method == mtx::events::msg::SASMethods::Emoji) {
-                                this->sasList = this->sas->generate_bytes_emoji(info);
-                                setState(CompareEmoji);
-                        } else if (this->method == mtx::events::msg::SASMethods::Decimal) {
-                                this->sasList = this->sas->generate_bytes_decimal(info);
-                                setState(CompareNumber);
-                        }
-                });
-
-        connect(
-          ChatPage::instance(),
-          &ChatPage::receivedDeviceVerificationMac,
-          this,
-          [this](const mtx::events::msg::KeyVerificationMac &msg) {
-                  nhlog::crypto()->info("verification: received mac");
-                  if (msg.transaction_id.has_value()) {
-                          if (msg.transaction_id.value() != this->transaction_id)
-                                  return;
-                  } else if (msg.relations.references()) {
-                          if (msg.relations.references() != this->relation.event_id)
-                                  return;
-                  }
-
-                  std::map<std::string, std::string> key_list;
-                  std::string key_string;
-                  for (const auto &mac : msg.mac) {
-                          for (const auto &[deviceid, key] : their_keys.device_keys) {
-                                  (void)deviceid;
-                                  if (key.keys.count(mac.first))
-                                          key_list[mac.first] = key.keys.at(mac.first);
-                          }
-
-                          if (their_keys.master_keys.keys.count(mac.first))
-                                  key_list[mac.first] = their_keys.master_keys.keys[mac.first];
-                          if (their_keys.user_signing_keys.keys.count(mac.first))
-                                  key_list[mac.first] =
-                                    their_keys.user_signing_keys.keys[mac.first];
-                          if (their_keys.self_signing_keys.keys.count(mac.first))
-                                  key_list[mac.first] =
-                                    their_keys.self_signing_keys.keys[mac.first];
-                  }
-                  auto macs = key_verification_mac(sas.get(),
-                                                   toClient,
-                                                   this->deviceId.toStdString(),
-                                                   http::client()->user_id(),
-                                                   http::client()->device_id(),
-                                                   this->transaction_id,
-                                                   key_list);
-
-                  for (const auto &[key, mac] : macs.mac) {
-                          if (mac != msg.mac.at(key)) {
-                                  this->cancelVerification(
-                                    DeviceVerificationFlow::Error::KeyMismatch);
-                                  return;
-                          }
-                  }
-
-                  if (msg.keys == macs.keys) {
-                          mtx::requests::KeySignaturesUpload req;
-                          if (utils::localUser().toStdString() == this->toClient.to_string()) {
-                                  // self verification, sign master key with device key, if we
-                                  // verified it
-                                  for (const auto &mac : msg.mac) {
-                                          if (their_keys.master_keys.keys.count(mac.first)) {
-                                                  json j = their_keys.master_keys;
-                                                  j.erase("signatures");
-                                                  j.erase("unsigned");
-                                                  mtx::crypto::CrossSigningKeys master_key = j;
-                                                  master_key
-                                                    .signatures[utils::localUser().toStdString()]
-                                                               ["ed25519:" +
-                                                                http::client()->device_id()] =
-                                                    olm::client()->sign_message(j.dump());
-                                                  req.signatures[utils::localUser().toStdString()]
-                                                                [master_key.keys.at(mac.first)] =
-                                                    master_key;
-                                          } else if (mac.first ==
-                                                     "ed25519:" + this->deviceId.toStdString()) {
-                                                  // Sign their device key with self signing key
-
-                                                  auto device_id = this->deviceId.toStdString();
-
-                                                  if (their_keys.device_keys.count(device_id)) {
-                                                          json j =
-                                                            their_keys.device_keys.at(device_id);
-                                                          j.erase("signatures");
-                                                          j.erase("unsigned");
-
-                                                          auto secret = cache::secret(
-                                                            mtx::secret_storage::secrets::
-                                                              cross_signing_self_signing);
-                                                          if (!secret)
-                                                                  continue;
-                                                          auto ssk =
-                                                            mtx::crypto::PkSigning::from_seed(
-                                                              *secret);
-
-                                                          mtx::crypto::DeviceKeys dev = j;
-                                                          dev.signatures
-                                                            [utils::localUser().toStdString()]
-                                                            ["ed25519:" + ssk.public_key()] =
-                                                            ssk.sign(j.dump());
-
-                                                          req.signatures[utils::localUser()
-                                                                           .toStdString()]
-                                                                        [device_id] = dev;
-                                                  }
-                                          }
-                                  }
-                          } else {
-                                  // Sign their master key with user signing key
-                                  for (const auto &mac : msg.mac) {
-                                          if (their_keys.master_keys.keys.count(mac.first)) {
-                                                  json j = their_keys.master_keys;
-                                                  j.erase("signatures");
-                                                  j.erase("unsigned");
-
-                                                  auto secret =
-                                                    cache::secret(mtx::secret_storage::secrets::
-                                                                    cross_signing_user_signing);
-                                                  if (!secret)
-                                                          continue;
-                                                  auto usk =
-                                                    mtx::crypto::PkSigning::from_seed(*secret);
-
-                                                  mtx::crypto::CrossSigningKeys master_key = j;
-                                                  master_key
-                                                    .signatures[utils::localUser().toStdString()]
-                                                               ["ed25519:" + usk.public_key()] =
-                                                    usk.sign(j.dump());
-
-                                                  req.signatures[toClient.to_string()]
-                                                                [master_key.keys.at(mac.first)] =
-                                                    master_key;
-                                          }
-                                  }
-                          }
-
-                          if (!req.signatures.empty()) {
-                                  http::client()->keys_signatures_upload(
-                                    req,
-                                    [](const mtx::responses::KeySignaturesUpload &res,
-                                       mtx::http::RequestErr err) {
-                                            if (err) {
-                                                    nhlog::net()->error(
-                                                      "failed to upload signatures: {},{}",
-                                                      mtx::errors::to_string(
-                                                        err->matrix_error.errcode),
-                                                      static_cast<int>(err->status_code));
-                                            }
-
-                                            for (const auto &[user_id, tmp] : res.errors)
-                                                    for (const auto &[key_id, e] : tmp)
-                                                            nhlog::net()->error(
-                                                              "signature error for user {} and key "
-                                                              "id {}: {}, {}",
-                                                              user_id,
-                                                              key_id,
-                                                              mtx::errors::to_string(e.errcode),
-                                                              e.error);
-                                    });
-                          }
-
-                          this->isMacVerified = true;
-                          this->acceptDevice();
-                  } else {
-                          this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
-                  }
-          });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationReady,
-                this,
-                [this](const mtx::events::msg::KeyVerificationReady &msg) {
-                        nhlog::crypto()->info("verification: received ready");
-                        if (!sender) {
-                                if (msg.from_device != http::client()->device_id()) {
-                                        error_ = User;
-                                        emit errorChanged();
-                                        setState(Failed);
-                                }
-
-                                return;
-                        }
-
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                                else {
-                                        this->deviceId = QString::fromStdString(msg.from_device);
-                                }
-                        }
-                        this->startVerificationRequest();
-                });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationDone,
-                this,
-                [this](const mtx::events::msg::KeyVerificationDone &msg) {
-                        nhlog::crypto()->info("verification: receoved done");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-                        nhlog::ui()->info("Flow done on other side");
-                });
-
-        timeout->start(TIMEOUT);
-}
-
-QString
-DeviceVerificationFlow::state()
-{
-        switch (state_) {
-        case PromptStartVerification:
-                return "PromptStartVerification";
-        case CompareEmoji:
-                return "CompareEmoji";
-        case CompareNumber:
-                return "CompareNumber";
-        case WaitingForKeys:
-                return "WaitingForKeys";
-        case WaitingForOtherToAccept:
-                return "WaitingForOtherToAccept";
-        case WaitingForMac:
-                return "WaitingForMac";
-        case Success:
-                return "Success";
-        case Failed:
-                return "Failed";
-        default:
-                return "";
-        }
-}
-
-void
-DeviceVerificationFlow::next()
-{
-        if (sender) {
-                switch (state_) {
-                case PromptStartVerification:
-                        sendVerificationRequest();
-                        break;
-                case CompareEmoji:
-                case CompareNumber:
-                        sendVerificationMac();
-                        break;
-                case WaitingForKeys:
-                case WaitingForOtherToAccept:
-                case WaitingForMac:
-                case Success:
-                case Failed:
-                        nhlog::db()->error("verification: Invalid state transition!");
-                        break;
-                }
-        } else {
-                switch (state_) {
-                case PromptStartVerification:
-                        if (canonical_json.is_null())
-                                sendVerificationReady();
-                        else // legacy path without request and ready
-                                acceptVerificationRequest();
-                        break;
-                case CompareEmoji:
-                        [[fallthrough]];
-                case CompareNumber:
-                        sendVerificationMac();
-                        break;
-                case WaitingForKeys:
-                case WaitingForOtherToAccept:
-                case WaitingForMac:
-                case Success:
-                case Failed:
-                        nhlog::db()->error("verification: Invalid state transition!");
-                        break;
-                }
-        }
-}
-
-QString
-DeviceVerificationFlow::getUserId()
-{
-        return QString::fromStdString(this->toClient.to_string());
-}
-
-QString
-DeviceVerificationFlow::getDeviceId()
-{
-        return this->deviceId;
-}
-
-bool
-DeviceVerificationFlow::getSender()
-{
-        return this->sender;
-}
-
-std::vector<int>
-DeviceVerificationFlow::getSasList()
-{
-        return this->sasList;
-}
-
-bool
-DeviceVerificationFlow::isSelfVerification() const
-{
-        return this->toClient.to_string() == http::client()->user_id().to_string();
-}
-
-void
-DeviceVerificationFlow::setEventId(std::string event_id_)
-{
-        this->relation.rel_type = mtx::common::RelationType::Reference;
-        this->relation.event_id = event_id_;
-        this->transaction_id    = event_id_;
-}
-
-void
-DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
-                                           std::string)
-{
-        if (msg.transaction_id.has_value()) {
-                if (msg.transaction_id.value() != this->transaction_id)
-                        return;
-        } else if (msg.relations.references()) {
-                if (msg.relations.references() != this->relation.event_id)
-                        return;
-        }
-        if ((std::find(msg.key_agreement_protocols.begin(),
-                       msg.key_agreement_protocols.end(),
-                       "curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
-            (std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
-            (std::find(msg.message_authentication_codes.begin(),
-                       msg.message_authentication_codes.end(),
-                       "hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
-                if (std::find(msg.short_authentication_string.begin(),
-                              msg.short_authentication_string.end(),
-                              mtx::events::msg::SASMethods::Emoji) !=
-                    msg.short_authentication_string.end()) {
-                        this->method = mtx::events::msg::SASMethods::Emoji;
-                } else if (std::find(msg.short_authentication_string.begin(),
-                                     msg.short_authentication_string.end(),
-                                     mtx::events::msg::SASMethods::Decimal) !=
-                           msg.short_authentication_string.end()) {
-                        this->method = mtx::events::msg::SASMethods::Decimal;
-                } else {
-                        this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
-                        return;
-                }
-                if (!sender)
-                        this->canonical_json = nlohmann::json(msg);
-                else {
-                        if (utils::localUser().toStdString() < this->toClient.to_string()) {
-                                this->canonical_json = nlohmann::json(msg);
-                        }
-                }
-
-                if (state_ != PromptStartVerification)
-                        this->acceptVerificationRequest();
-        } else {
-                this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
-        }
-}
-
-//! accepts a verification
-void
-DeviceVerificationFlow::acceptVerificationRequest()
-{
-        mtx::events::msg::KeyVerificationAccept req;
-
-        req.method                      = mtx::events::msg::VerificationMethods::SASv1;
-        req.key_agreement_protocol      = "curve25519-hkdf-sha256";
-        req.hash                        = "sha256";
-        req.message_authentication_code = "hkdf-hmac-sha256";
-        if (this->method == mtx::events::msg::SASMethods::Emoji)
-                req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
-        else if (this->method == mtx::events::msg::SASMethods::Decimal)
-                req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
-        req.commitment = mtx::crypto::bin2base64_unpadded(
-          mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
-
-        send(req);
-        setState(WaitingForKeys);
-}
-//! responds verification request
-void
-DeviceVerificationFlow::sendVerificationReady()
-{
-        mtx::events::msg::KeyVerificationReady req;
-
-        req.from_device = http::client()->device_id();
-        req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
-
-        send(req);
-        setState(WaitingForKeys);
-}
-//! accepts a verification
-void
-DeviceVerificationFlow::sendVerificationDone()
-{
-        mtx::events::msg::KeyVerificationDone req;
-
-        send(req);
-}
-//! starts the verification flow
-void
-DeviceVerificationFlow::startVerificationRequest()
-{
-        mtx::events::msg::KeyVerificationStart req;
-
-        req.from_device                  = http::client()->device_id();
-        req.method                       = mtx::events::msg::VerificationMethods::SASv1;
-        req.key_agreement_protocols      = {"curve25519-hkdf-sha256"};
-        req.hashes                       = {"sha256"};
-        req.message_authentication_codes = {"hkdf-hmac-sha256"};
-        req.short_authentication_string  = {mtx::events::msg::SASMethods::Decimal,
-                                           mtx::events::msg::SASMethods::Emoji};
-
-        if (this->type == DeviceVerificationFlow::Type::ToDevice) {
-                mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
-                req.transaction_id   = this->transaction_id;
-                this->canonical_json = nlohmann::json(req);
-        } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
-                req.relations.relations.push_back(this->relation);
-                // Set synthesized to surpress the nheko relation extensions
-                req.relations.synthesized = true;
-                this->canonical_json      = nlohmann::json(req);
-        }
-        send(req);
-        setState(WaitingForOtherToAccept);
-}
-//! sends a verification request
-void
-DeviceVerificationFlow::sendVerificationRequest()
-{
-        mtx::events::msg::KeyVerificationRequest req;
-
-        req.from_device = http::client()->device_id();
-        req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
-
-        if (this->type == DeviceVerificationFlow::Type::ToDevice) {
-                QDateTime currentTime = QDateTime::currentDateTimeUtc();
-
-                req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
-
-        } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
-                req.to      = this->toClient.to_string();
-                req.msgtype = "m.key.verification.request";
-                req.body = "User is requesting to verify keys with you. However, your client does "
-                           "not support this method, so you will need to use the legacy method of "
-                           "key verification.";
-        }
-
-        send(req);
-        setState(WaitingForOtherToAccept);
-}
-//! cancels a verification flow
-void
-DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
-{
-        if (state_ == State::Success || state_ == State::Failed)
-                return;
-
-        mtx::events::msg::KeyVerificationCancel req;
-
-        if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
-                req.code   = "m.unknown_method";
-                req.reason = "unknown method received";
-        } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
-                req.code   = "m.mismatched_commitment";
-                req.reason = "commitment didn't match";
-        } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
-                req.code   = "m.mismatched_sas";
-                req.reason = "sas didn't match";
-        } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
-                req.code   = "m.key_match";
-                req.reason = "keys did not match";
-        } else if (error_code == DeviceVerificationFlow::Error::Timeout) {
-                req.code   = "m.timeout";
-                req.reason = "timed out";
-        } else if (error_code == DeviceVerificationFlow::Error::User) {
-                req.code   = "m.user";
-                req.reason = "user cancelled the verification";
-        } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
-                req.code   = "m.unexpected_message";
-                req.reason = "received messages out of order";
-        }
-
-        this->error_ = error_code;
-        emit errorChanged();
-        this->setState(Failed);
-
-        send(req);
-}
-//! sends the verification key
-void
-DeviceVerificationFlow::sendVerificationKey()
-{
-        mtx::events::msg::KeyVerificationKey req;
-
-        req.key = this->sas->public_key();
-
-        send(req);
-}
-
-mtx::events::msg::KeyVerificationMac
-key_verification_mac(mtx::crypto::SAS *sas,
-                     mtx::identifiers::User sender,
-                     const std::string &senderDevice,
-                     mtx::identifiers::User receiver,
-                     const std::string &receiverDevice,
-                     const std::string &transactionId,
-                     std::map<std::string, std::string> keys)
-{
-        mtx::events::msg::KeyVerificationMac req;
-
-        std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
-                           receiver.to_string() + receiverDevice + transactionId;
-
-        std::string key_list;
-        bool first = true;
-        for (const auto &[key_id, key] : keys) {
-                req.mac[key_id] = sas->calculate_mac(key, info + key_id);
-
-                if (!first)
-                        key_list += ",";
-                key_list += key_id;
-                first = false;
-        }
-
-        req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
-
-        return req;
-}
-
-//! sends the mac of the keys
-void
-DeviceVerificationFlow::sendVerificationMac()
-{
-        std::map<std::string, std::string> key_list;
-        key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
-
-        // send our master key, if we trust it
-        if (!this->our_trusted_master_key.empty())
-                key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
-
-        mtx::events::msg::KeyVerificationMac req =
-          key_verification_mac(sas.get(),
-                               http::client()->user_id(),
-                               http::client()->device_id(),
-                               this->toClient,
-                               this->deviceId.toStdString(),
-                               this->transaction_id,
-                               key_list);
-
-        send(req);
-
-        setState(WaitingForMac);
-        acceptDevice();
-}
-//! Completes the verification flow
-void
-DeviceVerificationFlow::acceptDevice()
-{
-        if (!isMacVerified) {
-                setState(WaitingForMac);
-        } else if (state_ == WaitingForMac) {
-                cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
-                this->sendVerificationDone();
-                setState(Success);
-
-                // Request secrets. We should probably check somehow, if a device knowns about the
-                // secrets.
-                if (utils::localUser().toStdString() == this->toClient.to_string() &&
-                    (!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) ||
-                     !cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) {
-                        olm::request_cross_signing_keys();
-                }
-        }
-}
-
-void
-DeviceVerificationFlow::unverify()
-{
-        cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
-
-        emit refreshProfile();
-}
-
-QSharedPointer<DeviceVerificationFlow>
-DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
-                                              TimelineModel *timelineModel_,
-                                              const mtx::events::msg::KeyVerificationRequest &msg,
-                                              QString other_user_,
-                                              QString event_id_)
-{
-        QSharedPointer<DeviceVerificationFlow> flow(
-          new DeviceVerificationFlow(parent_,
-                                     Type::RoomMsg,
-                                     timelineModel_,
-                                     other_user_,
-                                     QString::fromStdString(msg.from_device)));
-
-        flow->setEventId(event_id_.toStdString());
-
-        if (std::find(msg.methods.begin(),
-                      msg.methods.end(),
-                      mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
-                flow->cancelVerification(UnknownMethod);
-        }
-
-        return flow;
-}
-QSharedPointer<DeviceVerificationFlow>
-DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
-                                                const mtx::events::msg::KeyVerificationRequest &msg,
-                                                QString other_user_,
-                                                QString txn_id_)
-{
-        QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
-          parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
-        flow->transaction_id = txn_id_.toStdString();
-
-        if (std::find(msg.methods.begin(),
-                      msg.methods.end(),
-                      mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
-                flow->cancelVerification(UnknownMethod);
-        }
-
-        return flow;
-}
-QSharedPointer<DeviceVerificationFlow>
-DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
-                                                const mtx::events::msg::KeyVerificationStart &msg,
-                                                QString other_user_,
-                                                QString txn_id_)
-{
-        QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
-          parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
-        flow->transaction_id = txn_id_.toStdString();
-
-        flow->handleStartMessage(msg, "");
-
-        return flow;
-}
-QSharedPointer<DeviceVerificationFlow>
-DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
-                                                 TimelineModel *timelineModel_,
-                                                 QString userid)
-{
-        QSharedPointer<DeviceVerificationFlow> flow(
-          new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
-        flow->sender = true;
-        return flow;
-}
-QSharedPointer<DeviceVerificationFlow>
-DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
-{
-        QSharedPointer<DeviceVerificationFlow> flow(
-          new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
-
-        flow->sender         = true;
-        flow->transaction_id = http::client()->generate_txn_id();
-
-        return flow;
-}
diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h
deleted file mode 100644
index 4685a450b5b99fad4afad6844c38d3333d8b073e..0000000000000000000000000000000000000000
--- a/src/DeviceVerificationFlow.h
+++ /dev/null
@@ -1,251 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QObject>
-
-#include <mtx/responses/crypto.hpp>
-#include <nlohmann/json.hpp>
-
-#include "CacheCryptoStructs.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Olm.h"
-#include "timeline/TimelineModel.h"
-
-class QTimer;
-
-using sas_ptr = std::unique_ptr<mtx::crypto::SAS>;
-
-// clang-format off
-/*
- * Stolen from fluffy chat :D
- *
- *      State         |   +-------------+                    +-----------+                                  |
- *                    |   | AliceDevice |                    | BobDevice |                                  |
- *                    |   | (sender)    |                    |           |                                  |
- *                    |   +-------------+                    +-----------+                                  |
- * promptStartVerify  |         |                                 |                                         |
- *                    |      o  | (m.key.verification.request)    |                                         |
- *                    |      p  |-------------------------------->| (ASK FOR VERIFICATION REQUEST)          |
- * waitForOtherAccept |      t  |                                 |                                         | promptStartVerify
- * &&                 |      i  |      (m.key.verification.ready) |                                         |
- * no commitment      |      o  |<--------------------------------|                                         |
- * &&                 |      n  |                                 |                                         |
- * no canonical_json  |      a  |      (m.key.verification.start) |                                         | waitingForKeys
- *                    |      l  |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
- *                    |         |                                 |                                         | && no canonical_json
- *                    |         | m.key.verification.start        |                                         |
- * waitForOtherAccept |         |-------------------------------->| (IF NOT ALREADY ASKED,                  |
- * &&                 |         |                                 |  ASK FOR VERIFICATION REQUEST)          | promptStartVerify, if not accepted
- * canonical_json     |         |       m.key.verification.accept |                                         |
- *                    |         |<--------------------------------|                                         |
- * waitForOtherAccept |         |                                 |                                         | waitingForKeys
- * &&                 |         | m.key.verification.key          |                                         | && canonical_json
- * commitment         |         |-------------------------------->|                                         | && commitment
- *                    |         |                                 |                                         |
- *                    |         |          m.key.verification.key |                                         |
- *                    |         |<--------------------------------|                                         |
- * compareEmoji/Number|         |                                 |                                         | compareEmoji/Number
- *                    |         |     COMPARE EMOJI / NUMBERS     |                                         |
- *                    |         |                                 |                                         |
- * waitingForMac      |         |     m.key.verification.mac      |                                         | waitingForMac
- *                    | success |<------------------------------->|  success                                |
- *                    |         |                                 |                                         |
- * success/fail       |         |         m.key.verification.done |                                         | success/fail
- *                    |         |<------------------------------->|                                         |
- */
-// clang-format on
-class DeviceVerificationFlow : public QObject
-{
-        Q_OBJECT
-        Q_PROPERTY(QString state READ state NOTIFY stateChanged)
-        Q_PROPERTY(Error error READ error NOTIFY errorChanged)
-        Q_PROPERTY(QString userId READ getUserId CONSTANT)
-        Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
-        Q_PROPERTY(bool sender READ getSender CONSTANT)
-        Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT)
-        Q_PROPERTY(bool isDeviceVerification READ isDeviceVerification CONSTANT)
-        Q_PROPERTY(bool isSelfVerification READ isSelfVerification CONSTANT)
-
-public:
-        enum State
-        {
-                PromptStartVerification,
-                WaitingForOtherToAccept,
-                WaitingForKeys,
-                CompareEmoji,
-                CompareNumber,
-                WaitingForMac,
-                Success,
-                Failed,
-        };
-        Q_ENUM(State)
-
-        enum Type
-        {
-                ToDevice,
-                RoomMsg
-        };
-
-        enum Error
-        {
-                UnknownMethod,
-                MismatchedCommitment,
-                MismatchedSAS,
-                KeyMismatch,
-                Timeout,
-                User,
-                OutOfOrder,
-        };
-        Q_ENUM(Error)
-
-        static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification(
-          QObject *parent_,
-          TimelineModel *timelineModel_,
-          const mtx::events::msg::KeyVerificationRequest &msg,
-          QString other_user_,
-          QString event_id_);
-        static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
-          QObject *parent_,
-          const mtx::events::msg::KeyVerificationRequest &msg,
-          QString other_user_,
-          QString txn_id_);
-        static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
-          QObject *parent_,
-          const mtx::events::msg::KeyVerificationStart &msg,
-          QString other_user_,
-          QString txn_id_);
-        static QSharedPointer<DeviceVerificationFlow>
-        InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
-        static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent,
-                                                                                 QString userid,
-                                                                                 QString device);
-
-        // getters
-        QString state();
-        Error error() { return error_; }
-        QString getUserId();
-        QString getDeviceId();
-        bool getSender();
-        std::vector<int> getSasList();
-        QString transactionId() { return QString::fromStdString(this->transaction_id); }
-        // setters
-        void setDeviceId(QString deviceID);
-        void setEventId(std::string event_id);
-        bool isDeviceVerification() const
-        {
-                return this->type == DeviceVerificationFlow::Type::ToDevice;
-        }
-        bool isSelfVerification() const;
-
-        void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
-
-public slots:
-        //! unverifies a device
-        void unverify();
-        //! Continues the flow
-        void next();
-        //! Cancel the flow
-        void cancel() { cancelVerification(User); }
-
-signals:
-        void refreshProfile();
-        void stateChanged();
-        void errorChanged();
-
-private:
-        DeviceVerificationFlow(QObject *,
-                               DeviceVerificationFlow::Type flow_type,
-                               TimelineModel *model,
-                               QString userID,
-                               QString deviceId_);
-        void setState(State state)
-        {
-                if (state != state_) {
-                        state_ = state;
-                        emit stateChanged();
-                }
-        }
-
-        void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
-        //! sends a verification request
-        void sendVerificationRequest();
-        //! accepts a verification request
-        void sendVerificationReady();
-        //! completes the verification flow();
-        void sendVerificationDone();
-        //! accepts a verification
-        void acceptVerificationRequest();
-        //! starts the verification flow
-        void startVerificationRequest();
-        //! cancels a verification flow
-        void cancelVerification(DeviceVerificationFlow::Error error_code);
-        //! sends the verification key
-        void sendVerificationKey();
-        //! sends the mac of the keys
-        void sendVerificationMac();
-        //! Completes the verification flow
-        void acceptDevice();
-
-        std::string transaction_id;
-
-        bool sender;
-        Type type;
-        mtx::identifiers::User toClient;
-        QString deviceId;
-
-        // public part of our master key, when trusted or empty
-        std::string our_trusted_master_key;
-
-        mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
-        QTimer *timeout                     = nullptr;
-        sas_ptr sas;
-        std::string mac_method;
-        std::string commitment;
-        nlohmann::json canonical_json;
-
-        std::vector<int> sasList;
-        UserKeyCache their_keys;
-        TimelineModel *model_;
-        mtx::common::Relation relation;
-
-        State state_ = PromptStartVerification;
-        Error error_ = UnknownMethod;
-
-        bool isMacVerified = false;
-
-        template<typename T>
-        void send(T msg)
-        {
-                if (this->type == DeviceVerificationFlow::Type::ToDevice) {
-                        mtx::requests::ToDeviceMessages<T> body;
-                        msg.transaction_id                           = this->transaction_id;
-                        body[this->toClient][deviceId.toStdString()] = msg;
-
-                        http::client()->send_to_device<T>(
-                          this->transaction_id, body, [](mtx::http::RequestErr err) {
-                                  if (err)
-                                          nhlog::net()->warn(
-                                            "failed to send verification to_device message: {} {}",
-                                            err->matrix_error.error,
-                                            static_cast<int>(err->status_code));
-                          });
-                } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
-                        if constexpr (!std::is_same_v<T,
-                                                      mtx::events::msg::KeyVerificationRequest>) {
-                                msg.relations.relations.push_back(this->relation);
-                                // Set synthesized to surpress the nheko relation extensions
-                                msg.relations.synthesized = true;
-                        }
-                        (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
-                }
-
-                nhlog::net()->debug(
-                  "Sent verification step: {} in state: {}",
-                  mtx::events::to_string(mtx::events::to_device_content_to_type<T>),
-                  state().toStdString());
-        }
-};
diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index 362bf4e9f2705b966be3831d3eea73ac3e91ac7c..d794a384fbdf373ae778b03f35c2f079b138fd49 100644
--- a/src/EventAccessors.cpp
+++ b/src/EventAccessors.cpp
@@ -16,463 +16,460 @@ using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op,
 
 struct IsStateEvent
 {
-        template<class T>
-        bool operator()(const mtx::events::StateEvent<T> &)
-        {
-                return true;
-        }
-        template<class T>
-        bool operator()(const mtx::events::Event<T> &)
-        {
-                return false;
-        }
+    template<class T>
+    bool operator()(const mtx::events::StateEvent<T> &)
+    {
+        return true;
+    }
+    template<class T>
+    bool operator()(const mtx::events::Event<T> &)
+    {
+        return false;
+    }
 };
 
 struct EventMsgType
 {
-        template<class E>
-        using msgtype_t = decltype(E::msgtype);
-        template<class T>
-        mtx::events::MessageType operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<msgtype_t, T>::value) {
-                        if constexpr (std::is_same_v<std::optional<std::string>,
-                                                     std::remove_cv_t<decltype(e.content.msgtype)>>)
-                                return mtx::events::getMessageType(e.content.msgtype.value());
-                        else if constexpr (std::is_same_v<
-                                             std::string,
-                                             std::remove_cv_t<decltype(e.content.msgtype)>>)
-                                return mtx::events::getMessageType(e.content.msgtype);
-                }
-                return mtx::events::MessageType::Unknown;
+    template<class E>
+    using msgtype_t = decltype(E::msgtype);
+    template<class T>
+    mtx::events::MessageType operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<msgtype_t, T>::value) {
+            if constexpr (std::is_same_v<std::optional<std::string>,
+                                         std::remove_cv_t<decltype(e.content.msgtype)>>)
+                return mtx::events::getMessageType(e.content.msgtype.value());
+            else if constexpr (std::is_same_v<std::string,
+                                              std::remove_cv_t<decltype(e.content.msgtype)>>)
+                return mtx::events::getMessageType(e.content.msgtype);
         }
+        return mtx::events::MessageType::Unknown;
+    }
 };
 
 struct EventRoomName
 {
-        template<class T>
-        std::string operator()(const T &e)
-        {
-                if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Name>, T>)
-                        return e.content.name;
-                return "";
-        }
+    template<class T>
+    std::string operator()(const T &e)
+    {
+        if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Name>, T>)
+            return e.content.name;
+        return "";
+    }
 };
 
 struct EventRoomTopic
 {
-        template<class T>
-        std::string operator()(const T &e)
-        {
-                if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Topic>, T>)
-                        return e.content.topic;
-                return "";
-        }
+    template<class T>
+    std::string operator()(const T &e)
+    {
+        if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Topic>, T>)
+            return e.content.topic;
+        return "";
+    }
 };
 
 struct CallType
 {
-        template<class T>
-        std::string operator()(const T &e)
-        {
-                if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>,
-                                             T>) {
-                        const char video[]     = "m=video";
-                        const std::string &sdp = e.content.sdp;
-                        return std::search(sdp.cbegin(),
-                                           sdp.cend(),
-                                           std::cbegin(video),
-                                           std::cend(video) - 1,
-                                           [](unsigned char c1, unsigned char c2) {
-                                                   return std::tolower(c1) == std::tolower(c2);
-                                           }) != sdp.cend()
-                                 ? "video"
-                                 : "voice";
-                }
-                return std::string();
+    template<class T>
+    std::string operator()(const T &e)
+    {
+        if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, T>) {
+            const char video[]     = "m=video";
+            const std::string &sdp = e.content.sdp;
+            return std::search(sdp.cbegin(),
+                               sdp.cend(),
+                               std::cbegin(video),
+                               std::cend(video) - 1,
+                               [](unsigned char c1, unsigned char c2) {
+                                   return std::tolower(c1) == std::tolower(c2);
+                               }) != sdp.cend()
+                     ? "video"
+                     : "voice";
         }
+        return std::string();
+    }
 };
 
 struct EventBody
 {
-        template<class C>
-        using body_t = decltype(C::body);
-        template<class T>
-        std::string operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<body_t, T>::value) {
-                        if constexpr (std::is_same_v<std::optional<std::string>,
-                                                     std::remove_cv_t<decltype(e.content.body)>>)
-                                return e.content.body ? e.content.body.value() : "";
-                        else if constexpr (std::is_same_v<
-                                             std::string,
-                                             std::remove_cv_t<decltype(e.content.body)>>)
-                                return e.content.body;
-                }
-                return "";
+    template<class C>
+    using body_t = decltype(C::body);
+    template<class T>
+    std::string operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<body_t, T>::value) {
+            if constexpr (std::is_same_v<std::optional<std::string>,
+                                         std::remove_cv_t<decltype(e.content.body)>>)
+                return e.content.body ? e.content.body.value() : "";
+            else if constexpr (std::is_same_v<std::string,
+                                              std::remove_cv_t<decltype(e.content.body)>>)
+                return e.content.body;
         }
+        return "";
+    }
 };
 
 struct EventFormattedBody
 {
-        template<class C>
-        using formatted_body_t = decltype(C::formatted_body);
-        template<class T>
-        std::string operator()(const mtx::events::RoomEvent<T> &e)
-        {
-                if constexpr (is_detected<formatted_body_t, T>::value) {
-                        if (e.content.format == "org.matrix.custom.html")
-                                return e.content.formatted_body;
-                }
-                return "";
+    template<class C>
+    using formatted_body_t = decltype(C::formatted_body);
+    template<class T>
+    std::string operator()(const mtx::events::RoomEvent<T> &e)
+    {
+        if constexpr (is_detected<formatted_body_t, T>::value) {
+            if (e.content.format == "org.matrix.custom.html")
+                return e.content.formatted_body;
         }
+        return "";
+    }
 };
 
 struct EventFile
 {
-        template<class Content>
-        using file_t = decltype(Content::file);
-        template<class T>
-        std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<file_t, T>::value)
-                        return e.content.file;
-                return std::nullopt;
-        }
+    template<class Content>
+    using file_t = decltype(Content::file);
+    template<class T>
+    std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<file_t, T>::value)
+            return e.content.file;
+        return std::nullopt;
+    }
 };
 
 struct EventUrl
 {
-        template<class Content>
-        using url_t = decltype(Content::url);
-        template<class T>
-        std::string operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<url_t, T>::value) {
-                        if (auto file = EventFile{}(e))
-                                return file->url;
-                        return e.content.url;
-                }
-                return "";
+    template<class Content>
+    using url_t = decltype(Content::url);
+    template<class T>
+    std::string operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<url_t, T>::value) {
+            if (auto file = EventFile{}(e))
+                return file->url;
+            return e.content.url;
         }
+        return "";
+    }
 };
 
 struct EventThumbnailUrl
 {
-        template<class Content>
-        using thumbnail_url_t = decltype(Content::info.thumbnail_url);
-        template<class T>
-        std::string operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<thumbnail_url_t, T>::value) {
-                        return e.content.info.thumbnail_url;
-                }
-                return "";
+    template<class Content>
+    using thumbnail_url_t = decltype(Content::info.thumbnail_url);
+    template<class T>
+    std::string operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<thumbnail_url_t, T>::value) {
+            return e.content.info.thumbnail_url;
         }
+        return "";
+    }
 };
 
 struct EventBlurhash
 {
-        template<class Content>
-        using blurhash_t = decltype(Content::info.blurhash);
-        template<class T>
-        std::string operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<blurhash_t, T>::value) {
-                        return e.content.info.blurhash;
-                }
-                return "";
+    template<class Content>
+    using blurhash_t = decltype(Content::info.blurhash);
+    template<class T>
+    std::string operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<blurhash_t, T>::value) {
+            return e.content.info.blurhash;
         }
+        return "";
+    }
 };
 
 struct EventFilename
 {
-        template<class T>
-        std::string operator()(const mtx::events::Event<T> &)
-        {
-                return "";
-        }
-        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e)
-        {
-                // body may be the original filename
-                return e.content.body;
-        }
-        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Video> &e)
-        {
-                // body may be the original filename
-                return e.content.body;
-        }
-        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Image> &e)
-        {
-                // body may be the original filename
-                return e.content.body;
-        }
-        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::File> &e)
-        {
-                // body may be the original filename
-                if (!e.content.filename.empty())
-                        return e.content.filename;
-                return e.content.body;
-        }
+    template<class T>
+    std::string operator()(const mtx::events::Event<T> &)
+    {
+        return "";
+    }
+    std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e)
+    {
+        // body may be the original filename
+        return e.content.body;
+    }
+    std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Video> &e)
+    {
+        // body may be the original filename
+        return e.content.body;
+    }
+    std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Image> &e)
+    {
+        // body may be the original filename
+        return e.content.body;
+    }
+    std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::File> &e)
+    {
+        // body may be the original filename
+        if (!e.content.filename.empty())
+            return e.content.filename;
+        return e.content.body;
+    }
 };
 
 struct EventMimeType
 {
-        template<class Content>
-        using mimetype_t = decltype(Content::info.mimetype);
-        template<class T>
-        std::string operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<mimetype_t, T>::value) {
-                        return e.content.info.mimetype;
-                }
-                return "";
+    template<class Content>
+    using mimetype_t = decltype(Content::info.mimetype);
+    template<class T>
+    std::string operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<mimetype_t, T>::value) {
+            return e.content.info.mimetype;
         }
+        return "";
+    }
 };
 
 struct EventFilesize
 {
-        template<class Content>
-        using filesize_t = decltype(Content::info.size);
-        template<class T>
-        int64_t operator()(const mtx::events::RoomEvent<T> &e)
-        {
-                if constexpr (is_detected<filesize_t, T>::value) {
-                        return e.content.info.size;
-                }
-                return 0;
+    template<class Content>
+    using filesize_t = decltype(Content::info.size);
+    template<class T>
+    int64_t operator()(const mtx::events::RoomEvent<T> &e)
+    {
+        if constexpr (is_detected<filesize_t, T>::value) {
+            return e.content.info.size;
         }
+        return 0;
+    }
 };
 
 struct EventRelations
 {
-        template<class Content>
-        using related_ev_id_t = decltype(Content::relations);
-        template<class T>
-        mtx::common::Relations operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<related_ev_id_t, T>::value) {
-                        return e.content.relations;
-                }
-                return {};
+    template<class Content>
+    using related_ev_id_t = decltype(Content::relations);
+    template<class T>
+    mtx::common::Relations operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<related_ev_id_t, T>::value) {
+            return e.content.relations;
         }
+        return {};
+    }
 };
 
 struct SetEventRelations
 {
-        mtx::common::Relations new_relations;
-        template<class Content>
-        using related_ev_id_t = decltype(Content::relations);
-        template<class T>
-        void operator()(mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<related_ev_id_t, T>::value) {
-                        e.content.relations = std::move(new_relations);
-                }
+    mtx::common::Relations new_relations;
+    template<class Content>
+    using related_ev_id_t = decltype(Content::relations);
+    template<class T>
+    void operator()(mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<related_ev_id_t, T>::value) {
+            e.content.relations = std::move(new_relations);
         }
+    }
 };
 
 struct EventTransactionId
 {
-        template<class T>
-        std::string operator()(const mtx::events::RoomEvent<T> &e)
-        {
-                return e.unsigned_data.transaction_id;
-        }
-        template<class T>
-        std::string operator()(const mtx::events::Event<T> &e)
-        {
-                return e.unsigned_data.transaction_id;
-        }
+    template<class T>
+    std::string operator()(const mtx::events::RoomEvent<T> &e)
+    {
+        return e.unsigned_data.transaction_id;
+    }
+    template<class T>
+    std::string operator()(const mtx::events::Event<T> &e)
+    {
+        return e.unsigned_data.transaction_id;
+    }
 };
 
 struct EventMediaHeight
 {
-        template<class Content>
-        using h_t = decltype(Content::info.h);
-        template<class T>
-        uint64_t operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<h_t, T>::value) {
-                        return e.content.info.h;
-                }
-                return -1;
+    template<class Content>
+    using h_t = decltype(Content::info.h);
+    template<class T>
+    uint64_t operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<h_t, T>::value) {
+            return e.content.info.h;
         }
+        return -1;
+    }
 };
 
 struct EventMediaWidth
 {
-        template<class Content>
-        using w_t = decltype(Content::info.w);
-        template<class T>
-        uint64_t operator()(const mtx::events::Event<T> &e)
-        {
-                if constexpr (is_detected<w_t, T>::value) {
-                        return e.content.info.w;
-                }
-                return -1;
+    template<class Content>
+    using w_t = decltype(Content::info.w);
+    template<class T>
+    uint64_t operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (is_detected<w_t, T>::value) {
+            return e.content.info.w;
         }
+        return -1;
+    }
 };
 
 template<class T>
 double
 eventPropHeight(const mtx::events::RoomEvent<T> &e)
 {
-        auto w = eventWidth(e);
-        if (w == 0)
-                w = 1;
+    auto w = eventWidth(e);
+    if (w == 0)
+        w = 1;
 
-        double prop = eventHeight(e) / (double)w;
+    double prop = eventHeight(e) / (double)w;
 
-        return prop > 0 ? prop : 1.;
+    return prop > 0 ? prop : 1.;
 }
 }
 
 std::string
 mtx::accessors::event_id(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto e) { return e.event_id; }, event);
+    return std::visit([](const auto e) { return e.event_id; }, event);
 }
 std::string
 mtx::accessors::room_id(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto e) { return e.room_id; }, event);
+    return std::visit([](const auto e) { return e.room_id; }, event);
 }
 
 std::string
 mtx::accessors::sender(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto e) { return e.sender; }, event);
+    return std::visit([](const auto e) { return e.sender; }, event);
 }
 
 QDateTime
 mtx::accessors::origin_server_ts(const mtx::events::collections::TimelineEvents &event)
 {
-        return QDateTime::fromMSecsSinceEpoch(
-          std::visit([](const auto e) { return e.origin_server_ts; }, event));
+    return QDateTime::fromMSecsSinceEpoch(
+      std::visit([](const auto e) { return e.origin_server_ts; }, event));
 }
 
 std::string
 mtx::accessors::filename(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventFilename{}, event);
+    return std::visit(EventFilename{}, event);
 }
 
 mtx::events::MessageType
 mtx::accessors::msg_type(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventMsgType{}, event);
+    return std::visit(EventMsgType{}, event);
 }
 std::string
 mtx::accessors::room_name(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventRoomName{}, event);
+    return std::visit(EventRoomName{}, event);
 }
 std::string
 mtx::accessors::room_topic(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventRoomTopic{}, event);
+    return std::visit(EventRoomTopic{}, event);
 }
 
 std::string
 mtx::accessors::call_type(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(CallType{}, event);
+    return std::visit(CallType{}, event);
 }
 
 std::string
 mtx::accessors::body(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventBody{}, event);
+    return std::visit(EventBody{}, event);
 }
 
 std::string
 mtx::accessors::formatted_body(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventFormattedBody{}, event);
+    return std::visit(EventFormattedBody{}, event);
 }
 
 QString
 mtx::accessors::formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event)
 {
-        auto formatted = formatted_body(event);
-        if (!formatted.empty())
-                return QString::fromStdString(formatted);
-        else
-                return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "<br>");
+    auto formatted = formatted_body(event);
+    if (!formatted.empty())
+        return QString::fromStdString(formatted);
+    else
+        return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "<br>");
 }
 
 std::optional<mtx::crypto::EncryptedFile>
 mtx::accessors::file(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventFile{}, event);
+    return std::visit(EventFile{}, event);
 }
 
 std::string
 mtx::accessors::url(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventUrl{}, event);
+    return std::visit(EventUrl{}, event);
 }
 std::string
 mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventThumbnailUrl{}, event);
+    return std::visit(EventThumbnailUrl{}, event);
 }
 std::string
 mtx::accessors::blurhash(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventBlurhash{}, event);
+    return std::visit(EventBlurhash{}, event);
 }
 std::string
 mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventMimeType{}, event);
+    return std::visit(EventMimeType{}, event);
 }
 mtx::common::Relations
 mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventRelations{}, event);
+    return std::visit(EventRelations{}, event);
 }
 
 void
 mtx::accessors::set_relations(mtx::events::collections::TimelineEvents &event,
                               mtx::common::Relations relations)
 {
-        std::visit(SetEventRelations{std::move(relations)}, event);
+    std::visit(SetEventRelations{std::move(relations)}, event);
 }
 
 std::string
 mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventTransactionId{}, event);
+    return std::visit(EventTransactionId{}, event);
 }
 
 int64_t
 mtx::accessors::filesize(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventFilesize{}, event);
+    return std::visit(EventFilesize{}, event);
 }
 
 uint64_t
 mtx::accessors::media_height(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventMediaHeight{}, event);
+    return std::visit(EventMediaHeight{}, event);
 }
 
 uint64_t
 mtx::accessors::media_width(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventMediaWidth{}, event);
+    return std::visit(EventMediaWidth{}, event);
 }
 
 nlohmann::json
 mtx::accessors::serialize_event(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto &e) { return nlohmann::json(e); }, event);
+    return std::visit([](const auto &e) { return nlohmann::json(e); }, event);
 }
 
 bool
 mtx::accessors::is_state_event(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(IsStateEvent{}, event);
+    return std::visit(IsStateEvent{}, event);
 }
diff --git a/src/EventAccessors.h b/src/EventAccessors.h
index a58c7de0a3b8ddfe53437515597100302691ad0f..c6b8e85449aa5157de41c2c5fa6354eaa09bfe19 100644
--- a/src/EventAccessors.h
+++ b/src/EventAccessors.h
@@ -14,24 +14,24 @@
 namespace nheko {
 struct nonesuch
 {
-        ~nonesuch()                = delete;
-        nonesuch(nonesuch const &) = delete;
-        void operator=(nonesuch const &) = delete;
+    ~nonesuch()                = delete;
+    nonesuch(nonesuch const &) = delete;
+    void operator=(nonesuch const &) = delete;
 };
 
 namespace detail {
 template<class Default, class AlwaysVoid, template<class...> class Op, class... Args>
 struct detector
 {
-        using value_t = std::false_type;
-        using type    = Default;
+    using value_t = std::false_type;
+    using type    = Default;
 };
 
 template<class Default, template<class...> class Op, class... Args>
 struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
 {
-        using value_t = std::true_type;
-        using type    = Op<Args...>;
+    using value_t = std::true_type;
+    using type    = Op<Args...>;
 };
 
 } // namespace detail
diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp
index 6392de22876807a3d7a589c6069c587c749e2e08..39e46f012c49366e7600b85f028317ec9bfddd3d 100644
--- a/src/ImagePackListModel.cpp
+++ b/src/ImagePackListModel.cpp
@@ -13,82 +13,81 @@ ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *paren
   : QAbstractListModel(parent)
   , room_id(roomId)
 {
-        auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt);
+    auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt);
 
-        for (const auto &pack : packs_) {
-                packs.push_back(
-                  QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
-        }
+    for (const auto &pack : packs_) {
+        packs.push_back(QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
+    }
 }
 
 int
 ImagePackListModel::rowCount(const QModelIndex &) const
 {
-        return (int)packs.size();
+    return (int)packs.size();
 }
 
 QHash<int, QByteArray>
 ImagePackListModel::roleNames() const
 {
-        return {
-          {Roles::DisplayName, "displayName"},
-          {Roles::AvatarUrl, "avatarUrl"},
-          {Roles::FromAccountData, "fromAccountData"},
-          {Roles::FromCurrentRoom, "fromCurrentRoom"},
-          {Roles::StateKey, "statekey"},
-          {Roles::RoomId, "roomid"},
-        };
+    return {
+      {Roles::DisplayName, "displayName"},
+      {Roles::AvatarUrl, "avatarUrl"},
+      {Roles::FromAccountData, "fromAccountData"},
+      {Roles::FromCurrentRoom, "fromCurrentRoom"},
+      {Roles::StateKey, "statekey"},
+      {Roles::RoomId, "roomid"},
+    };
 }
 
 QVariant
 ImagePackListModel::data(const QModelIndex &index, int role) const
 {
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                const auto &pack = packs.at(index.row());
-                switch (role) {
-                case Roles::DisplayName:
-                        return pack->packname();
-                case Roles::AvatarUrl:
-                        return pack->avatarUrl();
-                case Roles::FromAccountData:
-                        return pack->roomid().isEmpty();
-                case Roles::FromCurrentRoom:
-                        return pack->roomid().toStdString() == this->room_id;
-                case Roles::StateKey:
-                        return pack->statekey();
-                case Roles::RoomId:
-                        return pack->roomid();
-                default:
-                        return {};
-                }
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        const auto &pack = packs.at(index.row());
+        switch (role) {
+        case Roles::DisplayName:
+            return pack->packname();
+        case Roles::AvatarUrl:
+            return pack->avatarUrl();
+        case Roles::FromAccountData:
+            return pack->roomid().isEmpty();
+        case Roles::FromCurrentRoom:
+            return pack->roomid().toStdString() == this->room_id;
+        case Roles::StateKey:
+            return pack->statekey();
+        case Roles::RoomId:
+            return pack->roomid();
+        default:
+            return {};
         }
-        return {};
+    }
+    return {};
 }
 
 SingleImagePackModel *
 ImagePackListModel::packAt(int row)
 {
-        if (row < 0 || static_cast<size_t>(row) >= packs.size())
-                return {};
-        auto e = packs.at(row).get();
-        QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
-        return e;
+    if (row < 0 || static_cast<size_t>(row) >= packs.size())
+        return {};
+    auto e = packs.at(row).get();
+    QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
+    return e;
 }
 
 SingleImagePackModel *
 ImagePackListModel::newPack(bool inRoom)
 {
-        ImagePackInfo info{};
-        if (inRoom)
-                info.source_room = room_id;
-        return new SingleImagePackModel(info);
+    ImagePackInfo info{};
+    if (inRoom)
+        info.source_room = room_id;
+    return new SingleImagePackModel(info);
 }
 
 bool
 ImagePackListModel::containsAccountPack() const
 {
-        for (const auto &p : packs)
-                if (p->roomid().isEmpty())
-                        return true;
-        return false;
+    for (const auto &p : packs)
+        if (p->roomid().isEmpty())
+            return true;
+    return false;
 }
diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h
index 2aa5abb25e8200fe0485bdf7cb33f4bc529ef3c2..0b39729a2e249bfe0c5fbaa900c2ed415ec398dc 100644
--- a/src/ImagePackListModel.h
+++ b/src/ImagePackListModel.h
@@ -11,31 +11,31 @@
 class SingleImagePackModel;
 class ImagePackListModel : public QAbstractListModel
 {
-        Q_OBJECT
-        Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
+    Q_OBJECT
+    Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
 public:
-        enum Roles
-        {
-                DisplayName = Qt::UserRole,
-                AvatarUrl,
-                FromAccountData,
-                FromCurrentRoom,
-                StateKey,
-                RoomId,
-        };
-
-        ImagePackListModel(const std::string &roomId, QObject *parent = nullptr);
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-        QVariant data(const QModelIndex &index, int role) const override;
-
-        Q_INVOKABLE SingleImagePackModel *packAt(int row);
-        Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom);
-
-        bool containsAccountPack() const;
+    enum Roles
+    {
+        DisplayName = Qt::UserRole,
+        AvatarUrl,
+        FromAccountData,
+        FromCurrentRoom,
+        StateKey,
+        RoomId,
+    };
+
+    ImagePackListModel(const std::string &roomId, QObject *parent = nullptr);
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+    QVariant data(const QModelIndex &index, int role) const override;
+
+    Q_INVOKABLE SingleImagePackModel *packAt(int row);
+    Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom);
+
+    bool containsAccountPack() const;
 
 private:
-        std::string room_id;
+    std::string room_id;
 
-        std::vector<QSharedPointer<SingleImagePackModel>> packs;
+    std::vector<QSharedPointer<SingleImagePackModel>> packs;
 };
diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp
index 27b2116f020aed1d46e46ee3a29dc1a32384d92f..e045581a9c658382944c861144c8a9ecb2d2151f 100644
--- a/src/InviteesModel.cpp
+++ b/src/InviteesModel.cpp
@@ -16,69 +16,68 @@ InviteesModel::InviteesModel(QObject *parent)
 void
 InviteesModel::addUser(QString mxid)
 {
-        beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count());
+    beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count());
 
-        auto invitee        = new Invitee{mxid, this};
-        auto indexOfInvitee = invitees_.count();
-        connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() {
-                emit dataChanged(index(indexOfInvitee), index(indexOfInvitee));
-        });
+    auto invitee        = new Invitee{mxid, this};
+    auto indexOfInvitee = invitees_.count();
+    connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() {
+        emit dataChanged(index(indexOfInvitee), index(indexOfInvitee));
+    });
 
-        invitees_.push_back(invitee);
+    invitees_.push_back(invitee);
 
-        endInsertRows();
-        emit countChanged();
+    endInsertRows();
+    emit countChanged();
 }
 
 QHash<int, QByteArray>
 InviteesModel::roleNames() const
 {
-        return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}};
+    return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}};
 }
 
 QVariant
 InviteesModel::data(const QModelIndex &index, int role) const
 {
-        if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0)
-                return {};
+    if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0)
+        return {};
 
-        switch (role) {
-        case Mxid:
-                return invitees_[index.row()]->mxid_;
-        case DisplayName:
-                return invitees_[index.row()]->displayName_;
-        case AvatarUrl:
-                return invitees_[index.row()]->avatarUrl_;
-        default:
-                return {};
-        }
+    switch (role) {
+    case Mxid:
+        return invitees_[index.row()]->mxid_;
+    case DisplayName:
+        return invitees_[index.row()]->displayName_;
+    case AvatarUrl:
+        return invitees_[index.row()]->avatarUrl_;
+    default:
+        return {};
+    }
 }
 
 QStringList
 InviteesModel::mxids()
 {
-        QStringList mxidList;
-        for (int i = 0; i < invitees_.length(); ++i)
-                mxidList.push_back(invitees_[i]->mxid_);
-        return mxidList;
+    QStringList mxidList;
+    for (int i = 0; i < invitees_.length(); ++i)
+        mxidList.push_back(invitees_[i]->mxid_);
+    return mxidList;
 }
 
 Invitee::Invitee(const QString &mxid, QObject *parent)
   : QObject{parent}
   , mxid_{mxid}
 {
-        http::client()->get_profile(
-          mxid_.toStdString(),
-          [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to retrieve profile info");
-                          emit userInfoLoaded();
-                          return;
-                  }
+    http::client()->get_profile(
+      mxid_.toStdString(), [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to retrieve profile info");
+              emit userInfoLoaded();
+              return;
+          }
 
-                  displayName_ = QString::fromStdString(res.display_name);
-                  avatarUrl_   = QString::fromStdString(res.avatar_url);
+          displayName_ = QString::fromStdString(res.display_name);
+          avatarUrl_   = QString::fromStdString(res.avatar_url);
 
-                  emit userInfoLoaded();
-          });
+          emit userInfoLoaded();
+      });
 }
diff --git a/src/InviteesModel.h b/src/InviteesModel.h
index a4e19ebbf89a517bf3dd394b312a1e777e433483..fd64116b30ba42ffc208c3214ab997870a40f922 100644
--- a/src/InviteesModel.h
+++ b/src/InviteesModel.h
@@ -10,54 +10,54 @@
 
 class Invitee : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        Invitee(const QString &mxid, QObject *parent = nullptr);
+    Invitee(const QString &mxid, QObject *parent = nullptr);
 
 signals:
-        void userInfoLoaded();
+    void userInfoLoaded();
 
 private:
-        const QString mxid_;
-        QString displayName_;
-        QString avatarUrl_;
+    const QString mxid_;
+    QString displayName_;
+    QString avatarUrl_;
 
-        friend class InviteesModel;
+    friend class InviteesModel;
 };
 
 class InviteesModel : public QAbstractListModel
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+    Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
 
 public:
-        enum Roles
-        {
-                Mxid,
-                DisplayName,
-                AvatarUrl,
-        };
+    enum Roles
+    {
+        Mxid,
+        DisplayName,
+        AvatarUrl,
+    };
 
-        InviteesModel(QObject *parent = nullptr);
+    InviteesModel(QObject *parent = nullptr);
 
-        Q_INVOKABLE void addUser(QString mxid);
+    Q_INVOKABLE void addUser(QString mxid);
 
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex & = QModelIndex()) const override
-        {
-                return (int)invitees_.size();
-        }
-        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
-        QStringList mxids();
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex & = QModelIndex()) const override
+    {
+        return (int)invitees_.size();
+    }
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+    QStringList mxids();
 
 signals:
-        void accept();
-        void countChanged();
+    void accept();
+    void countChanged();
 
 private:
-        QVector<Invitee *> invitees_;
+    QVector<Invitee *> invitees_;
 };
 
 #endif // INVITEESMODEL_H
diff --git a/src/JdenticonProvider.cpp b/src/JdenticonProvider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e282828637a5d6f981ac4d988492d1d59e937151
--- /dev/null
+++ b/src/JdenticonProvider.cpp
@@ -0,0 +1,112 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "JdenticonProvider.h"
+
+#include <QApplication>
+#include <QDir>
+#include <QPainter>
+#include <QPainterPath>
+#include <QPluginLoader>
+#include <QSvgRenderer>
+
+#include <mtxclient/crypto/client.hpp>
+
+#include "Cache.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "Utils.h"
+#include "jdenticoninterface.h"
+
+static QPixmap
+clipRadius(QPixmap img, double radius)
+{
+    QPixmap out(img.size());
+    out.fill(Qt::transparent);
+
+    QPainter painter(&out);
+    painter.setRenderHint(QPainter::Antialiasing, true);
+    painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
+
+    QPainterPath ppath;
+    ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
+
+    painter.setClipPath(ppath);
+    painter.drawPixmap(img.rect(), img);
+
+    return out;
+}
+
+JdenticonResponse::JdenticonResponse(const QString &key,
+                                     bool crop,
+                                     double radius,
+                                     const QSize &requestedSize)
+  : m_key(key)
+  , m_crop{crop}
+  , m_radius{radius}
+  , m_requestedSize(requestedSize.isValid() ? requestedSize : QSize(100, 100))
+  , m_pixmap{m_requestedSize}
+  , jdenticonInterface_{Jdenticon::getJdenticonInterface()}
+{
+    setAutoDelete(false);
+}
+
+void
+JdenticonResponse::run()
+{
+    m_pixmap.fill(Qt::transparent);
+
+    QPainter painter;
+    painter.begin(&m_pixmap);
+    painter.setRenderHint(QPainter::Antialiasing, true);
+    painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
+
+    try {
+        QSvgRenderer renderer{
+          jdenticonInterface_->generate(m_key, m_requestedSize.width()).toUtf8()};
+        renderer.render(&painter);
+    } catch (std::exception &e) {
+        nhlog::ui()->error(
+          "caught {} in jdenticonprovider, key '{}'", e.what(), m_key.toStdString());
+    }
+
+    painter.end();
+
+    m_pixmap = clipRadius(m_pixmap, m_radius);
+
+    emit finished();
+}
+
+namespace Jdenticon {
+JdenticonInterface *
+getJdenticonInterface()
+{
+    static JdenticonInterface *interface = nullptr;
+    static bool interfaceExists{true};
+
+    if (interface == nullptr && interfaceExists) {
+        QDir pluginsDir(qApp->applicationDirPath());
+
+        bool plugins = pluginsDir.cd("plugins");
+        if (plugins) {
+            for (const QString &fileName : pluginsDir.entryList(QDir::Files)) {
+                QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
+                QObject *plugin = pluginLoader.instance();
+                if (plugin) {
+                    interface = qobject_cast<JdenticonInterface *>(plugin);
+                    if (interface) {
+                        nhlog::ui()->info("Loaded jdenticon plugin.");
+                        break;
+                    }
+                }
+            }
+        } else {
+            nhlog::ui()->info("jdenticon plugin not found.");
+            interfaceExists = false;
+        }
+    }
+
+    return interface;
+}
+}
diff --git a/src/JdenticonProvider.h b/src/JdenticonProvider.h
new file mode 100644
index 0000000000000000000000000000000000000000..f4ef6d10143f487912253dc1413913236e928c88
--- /dev/null
+++ b/src/JdenticonProvider.h
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QImage>
+#include <QQuickAsyncImageProvider>
+#include <QQuickImageResponse>
+#include <QThreadPool>
+
+#include <mtx/common.hpp>
+
+#include "jdenticoninterface.h"
+
+namespace Jdenticon {
+JdenticonInterface *
+getJdenticonInterface();
+}
+
+class JdenticonResponse
+  : public QQuickImageResponse
+  , public QRunnable
+{
+public:
+    JdenticonResponse(const QString &key, bool crop, double radius, const QSize &requestedSize);
+
+    QQuickTextureFactory *textureFactory() const override
+    {
+        return QQuickTextureFactory::textureFactoryForImage(m_pixmap.toImage());
+    }
+
+    void run() override;
+
+    QString m_key;
+    bool m_crop;
+    double m_radius;
+    QSize m_requestedSize;
+    QPixmap m_pixmap;
+    JdenticonInterface *jdenticonInterface_ = nullptr;
+};
+
+class JdenticonProvider
+  : public QObject
+  , public QQuickAsyncImageProvider
+{
+    Q_OBJECT
+
+public:
+    static bool isAvailable() { return Jdenticon::getJdenticonInterface() != nullptr; }
+
+public slots:
+    QQuickImageResponse *requestImageResponse(const QString &id,
+                                              const QSize &requestedSize) override
+    {
+        auto id_      = id;
+        bool crop     = true;
+        double radius = 0;
+
+        auto queryStart = id.lastIndexOf('?');
+        if (queryStart != -1) {
+            id_            = id.left(queryStart);
+            auto query     = id.midRef(queryStart + 1);
+            auto queryBits = query.split('&');
+
+            for (auto b : queryBits) {
+                if (b.startsWith("radius=")) {
+                    radius = b.mid(7).toDouble();
+                }
+            }
+        }
+
+        JdenticonResponse *response = new JdenticonResponse(id_, crop, radius, requestedSize);
+        pool.start(response);
+        return response;
+    }
+
+private:
+    QThreadPool pool;
+};
diff --git a/src/Logging.cpp b/src/Logging.cpp
index 67bcaf7ae1b45e1c8087024ae367e7a270d4b711..a18a1ceeeec5cbefe7f311bdaa2fbd068caa331c 100644
--- a/src/Logging.cpp
+++ b/src/Logging.cpp
@@ -25,35 +25,35 @@ 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 : "";
-
-        if (
-          // The default style has the point size set. If you use pixel size anywhere, you get
-          // that warning, which is useless, since sometimes you need the pixel size to match the
-          // text to the size of the outer element for example. This is done in the avatar and
-          // without that you get one warning for every Avatar displayed, which is stupid!
-          msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size.")))
-                return;
-
-        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;
-        }
+    std::string localMsg = msg.toStdString();
+    const char *file     = context.file ? context.file : "";
+    const char *function = context.function ? context.function : "";
+
+    if (
+      // The default style has the point size set. If you use pixel size anywhere, you get
+      // that warning, which is useless, since sometimes you need the pixel size to match the
+      // text to the size of the outer element for example. This is done in the avatar and
+      // without that you get one warning for every Avatar displayed, which is stupid!
+      msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size.")))
+        return;
+
+    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;
+    }
 }
 }
 
@@ -63,60 +63,59 @@ bool enable_debug_log_from_commandline = false;
 void
 init(const std::string &file_path)
 {
-        auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
-          file_path, MAX_FILE_SIZE, MAX_LOG_FILES);
-
-        auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
-
-        std::vector<spdlog::sink_ptr> sinks;
-        sinks.push_back(file_sink);
-        sinks.push_back(console_sink);
-
-        net_logger = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks));
-        ui_logger  = std::make_shared<spdlog::logger>("ui", std::begin(sinks), std::end(sinks));
-        db_logger  = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
-        crypto_logger =
-          std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
-        qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks));
-
-        if (nheko::enable_debug_log || enable_debug_log_from_commandline) {
-                db_logger->set_level(spdlog::level::trace);
-                ui_logger->set_level(spdlog::level::trace);
-                crypto_logger->set_level(spdlog::level::trace);
-                net_logger->set_level(spdlog::level::trace);
-                qml_logger->set_level(spdlog::level::trace);
-        }
-
-        qInstallMessageHandler(qmlMessageHandler);
+    auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
+      file_path, MAX_FILE_SIZE, MAX_LOG_FILES);
+
+    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
+
+    std::vector<spdlog::sink_ptr> sinks;
+    sinks.push_back(file_sink);
+    sinks.push_back(console_sink);
+
+    net_logger    = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks));
+    ui_logger     = std::make_shared<spdlog::logger>("ui", std::begin(sinks), std::end(sinks));
+    db_logger     = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
+    crypto_logger = std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
+    qml_logger    = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks));
+
+    if (nheko::enable_debug_log || enable_debug_log_from_commandline) {
+        db_logger->set_level(spdlog::level::trace);
+        ui_logger->set_level(spdlog::level::trace);
+        crypto_logger->set_level(spdlog::level::trace);
+        net_logger->set_level(spdlog::level::trace);
+        qml_logger->set_level(spdlog::level::trace);
+    }
+
+    qInstallMessageHandler(qmlMessageHandler);
 }
 
 std::shared_ptr<spdlog::logger>
 ui()
 {
-        return ui_logger;
+    return ui_logger;
 }
 
 std::shared_ptr<spdlog::logger>
 net()
 {
-        return net_logger;
+    return net_logger;
 }
 
 std::shared_ptr<spdlog::logger>
 db()
 {
-        return db_logger;
+    return db_logger;
 }
 
 std::shared_ptr<spdlog::logger>
 crypto()
 {
-        return crypto_logger;
+    return crypto_logger;
 }
 
 std::shared_ptr<spdlog::logger>
 qml()
 {
-        return qml_logger;
+    return qml_logger;
 }
 }
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index f53d81ba325ec2bb5b775cd30d1c624036f77fcc..64e9c8650014b0d82d98e38d4015434fc8d5766e 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -34,477 +34,468 @@ LoginPage::LoginPage(QWidget *parent)
   : QWidget(parent)
   , inferredServerAddress_()
 {
-        qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
+    qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
 
-        top_layout_ = new QVBoxLayout();
+    top_layout_ = new QVBoxLayout();
+
+    top_bar_layout_ = new QHBoxLayout();
+    top_bar_layout_->setSpacing(0);
+    top_bar_layout_->setMargin(0);
+
+    back_button_ = new FlatButton(this);
+    back_button_->setMinimumSize(QSize(30, 30));
 
-        top_bar_layout_ = new QHBoxLayout();
-        top_bar_layout_->setSpacing(0);
-        top_bar_layout_->setMargin(0);
-
-        back_button_ = new FlatButton(this);
-        back_button_->setMinimumSize(QSize(30, 30));
-
-        top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
-        top_bar_layout_->addStretch(1);
-
-        QIcon icon;
-        icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png");
-
-        back_button_->setIcon(icon);
-        back_button_->setIconSize(QSize(32, 32));
-
-        QIcon logo;
-        logo.addFile(":/logos/login.png");
-
-        logo_ = new QLabel(this);
-        logo_->setPixmap(logo.pixmap(128));
-
-        logo_layout_ = new QHBoxLayout();
-        logo_layout_->setContentsMargins(0, 0, 0, 20);
-        logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
-
-        form_wrapper_ = new QHBoxLayout();
-        form_widget_  = new QWidget();
-        form_widget_->setMinimumSize(QSize(350, 200));
-
-        form_layout_ = new QVBoxLayout();
-        form_layout_->setSpacing(20);
-        form_layout_->setContentsMargins(0, 0, 0, 30);
-        form_widget_->setLayout(form_layout_);
-
-        form_wrapper_->addStretch(1);
-        form_wrapper_->addWidget(form_widget_);
-        form_wrapper_->addStretch(1);
-
-        matrixid_input_ = new TextField(this);
-        matrixid_input_->setLabel(tr("Matrix ID"));
-        matrixid_input_->setRegexp(QRegularExpression("@.+?:.{3,}"));
-        matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org"));
-        matrixid_input_->setToolTip(
-          tr("Your login name. A mxid should start with @ followed by the user id. After the user "
-             "id you need to include your server name after a :.\nYou can also put your homeserver "
-             "address there, if your server doesn't support .well-known lookup.\nExample: "
-             "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a "
-             "field to enter the server manually."));
-
-        spinner_ = new LoadingIndicator(this);
-        spinner_->setFixedHeight(40);
-        spinner_->setFixedWidth(40);
-        spinner_->hide();
-
-        errorIcon_ = new QLabel(this);
-        errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png"));
-        errorIcon_->hide();
-
-        matrixidLayout_ = new QHBoxLayout();
-        matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter);
-
-        QFont font;
-
-        error_matrixid_label_ = new QLabel(this);
-        error_matrixid_label_->setFont(font);
-        error_matrixid_label_->setWordWrap(true);
-
-        password_input_ = new TextField(this);
-        password_input_->setLabel(tr("Password"));
-        password_input_->setEchoMode(QLineEdit::Password);
-        password_input_->setToolTip(tr("Your password."));
-
-        deviceName_ = new TextField(this);
-        deviceName_->setLabel(tr("Device name"));
-        deviceName_->setToolTip(
-          tr("A name for this device, which will be shown to others, when verifying your devices. "
-             "If none is provided a default is used."));
-
-        serverInput_ = new TextField(this);
-        serverInput_->setLabel(tr("Homeserver address"));
-        serverInput_->setPlaceholderText(tr("server.my:8787"));
-        serverInput_->setToolTip(tr("The address that can be used to contact you homeservers "
-                                    "client API.\nExample: https://server.my:8787"));
-        serverInput_->hide();
-
-        serverLayout_ = new QHBoxLayout();
-        serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter);
-
-        form_layout_->addLayout(matrixidLayout_);
-        form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter);
-        form_layout_->addWidget(password_input_);
-        form_layout_->addWidget(deviceName_, Qt::AlignHCenter);
-        form_layout_->addLayout(serverLayout_);
-
-        error_matrixid_label_->hide();
-
-        button_layout_ = new QHBoxLayout();
-        button_layout_->setSpacing(20);
-        button_layout_->setContentsMargins(0, 0, 0, 30);
-
-        login_button_ = new RaisedButton(tr("LOGIN"), this);
-        login_button_->setMinimumSize(150, 65);
-        login_button_->setFontSize(20);
-        login_button_->setCornerRadius(3);
-
-        sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this);
-        sso_login_button_->setMinimumSize(150, 65);
-        sso_login_button_->setFontSize(20);
-        sso_login_button_->setCornerRadius(3);
-        sso_login_button_->setVisible(false);
-
-        button_layout_->addStretch(1);
-        button_layout_->addWidget(login_button_);
-        button_layout_->addWidget(sso_login_button_);
-        button_layout_->addStretch(1);
-
-        error_label_ = new QLabel(this);
-        error_label_->setFont(font);
-        error_label_->setWordWrap(true);
-
-        top_layout_->addLayout(top_bar_layout_);
-        top_layout_->addStretch(1);
-        top_layout_->addLayout(logo_layout_);
-        top_layout_->addLayout(form_wrapper_);
-        top_layout_->addStretch(1);
-        top_layout_->addLayout(button_layout_);
-        top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
-        top_layout_->addStretch(1);
-
-        setLayout(top_layout_);
-
-        connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection);
-        connect(
-          this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
-
-        connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
-        connect(login_button_, &RaisedButton::clicked, this, [this]() {
-                onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO);
-        });
-        connect(sso_login_button_, &RaisedButton::clicked, this, [this]() {
-                onLoginButtonClicked(LoginMethod::SSO);
-        });
-        connect(this,
-                &LoginPage::showErrorMessage,
-                this,
-                static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError),
-                Qt::QueuedConnection);
-        connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-        connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-        connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-        connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-        connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered()));
-        connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
+    top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
+    top_bar_layout_->addStretch(1);
+
+    QIcon icon;
+    icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png");
+
+    back_button_->setIcon(icon);
+    back_button_->setIconSize(QSize(32, 32));
+
+    QIcon logo;
+    logo.addFile(":/logos/login.png");
+
+    logo_ = new QLabel(this);
+    logo_->setPixmap(logo.pixmap(128));
+
+    logo_layout_ = new QHBoxLayout();
+    logo_layout_->setContentsMargins(0, 0, 0, 20);
+    logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
+
+    form_wrapper_ = new QHBoxLayout();
+    form_widget_  = new QWidget();
+    form_widget_->setMinimumSize(QSize(350, 200));
+
+    form_layout_ = new QVBoxLayout();
+    form_layout_->setSpacing(20);
+    form_layout_->setContentsMargins(0, 0, 0, 30);
+    form_widget_->setLayout(form_layout_);
+
+    form_wrapper_->addStretch(1);
+    form_wrapper_->addWidget(form_widget_);
+    form_wrapper_->addStretch(1);
+
+    matrixid_input_ = new TextField(this);
+    matrixid_input_->setLabel(tr("Matrix ID"));
+    matrixid_input_->setRegexp(QRegularExpression("@.+?:.{3,}"));
+    matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org"));
+    matrixid_input_->setToolTip(
+      tr("Your login name. A mxid should start with @ followed by the user id. After the user "
+         "id you need to include your server name after a :.\nYou can also put your homeserver "
+         "address there, if your server doesn't support .well-known lookup.\nExample: "
+         "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a "
+         "field to enter the server manually."));
+
+    spinner_ = new LoadingIndicator(this);
+    spinner_->setFixedHeight(40);
+    spinner_->setFixedWidth(40);
+    spinner_->hide();
+
+    errorIcon_ = new QLabel(this);
+    errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png"));
+    errorIcon_->hide();
+
+    matrixidLayout_ = new QHBoxLayout();
+    matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter);
+
+    QFont font;
+
+    error_matrixid_label_ = new QLabel(this);
+    error_matrixid_label_->setFont(font);
+    error_matrixid_label_->setWordWrap(true);
+
+    password_input_ = new TextField(this);
+    password_input_->setLabel(tr("Password"));
+    password_input_->setEchoMode(QLineEdit::Password);
+    password_input_->setToolTip(tr("Your password."));
+
+    deviceName_ = new TextField(this);
+    deviceName_->setLabel(tr("Device name"));
+    deviceName_->setToolTip(
+      tr("A name for this device, which will be shown to others, when verifying your devices. "
+         "If none is provided a default is used."));
+
+    serverInput_ = new TextField(this);
+    serverInput_->setLabel(tr("Homeserver address"));
+    serverInput_->setPlaceholderText(tr("server.my:8787"));
+    serverInput_->setToolTip(tr("The address that can be used to contact you homeservers "
+                                "client API.\nExample: https://server.my:8787"));
+    serverInput_->hide();
+
+    serverLayout_ = new QHBoxLayout();
+    serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter);
+
+    form_layout_->addLayout(matrixidLayout_);
+    form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter);
+    form_layout_->addWidget(password_input_);
+    form_layout_->addWidget(deviceName_, Qt::AlignHCenter);
+    form_layout_->addLayout(serverLayout_);
+
+    error_matrixid_label_->hide();
+
+    button_layout_ = new QHBoxLayout();
+    button_layout_->setSpacing(20);
+    button_layout_->setContentsMargins(0, 0, 0, 30);
+
+    login_button_ = new RaisedButton(tr("LOGIN"), this);
+    login_button_->setMinimumSize(150, 65);
+    login_button_->setFontSize(20);
+    login_button_->setCornerRadius(3);
+
+    sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this);
+    sso_login_button_->setMinimumSize(150, 65);
+    sso_login_button_->setFontSize(20);
+    sso_login_button_->setCornerRadius(3);
+    sso_login_button_->setVisible(false);
+
+    button_layout_->addStretch(1);
+    button_layout_->addWidget(login_button_);
+    button_layout_->addWidget(sso_login_button_);
+    button_layout_->addStretch(1);
+
+    error_label_ = new QLabel(this);
+    error_label_->setFont(font);
+    error_label_->setWordWrap(true);
+
+    top_layout_->addLayout(top_bar_layout_);
+    top_layout_->addStretch(1);
+    top_layout_->addLayout(logo_layout_);
+    top_layout_->addLayout(form_wrapper_);
+    top_layout_->addStretch(1);
+    top_layout_->addLayout(button_layout_);
+    top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
+    top_layout_->addStretch(1);
+
+    setLayout(top_layout_);
+
+    connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection);
+    connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
+
+    connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
+    connect(login_button_, &RaisedButton::clicked, this, [this]() {
+        onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO);
+    });
+    connect(sso_login_button_, &RaisedButton::clicked, this, [this]() {
+        onLoginButtonClicked(LoginMethod::SSO);
+    });
+    connect(this,
+            &LoginPage::showErrorMessage,
+            this,
+            static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError),
+            Qt::QueuedConnection);
+    connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
+    connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
+    connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
+    connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
+    connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered()));
+    connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
 }
 void
 LoginPage::showError(const QString &msg)
 {
-        auto rect  = QFontMetrics(font()).boundingRect(msg);
-        int width  = rect.width();
-        int height = rect.height();
-        error_label_->setFixedHeight((int)qCeil(width / 200.0) * height);
-        error_label_->setText(msg);
+    auto rect  = QFontMetrics(font()).boundingRect(msg);
+    int width  = rect.width();
+    int height = rect.height();
+    error_label_->setFixedHeight((int)qCeil(width / 200.0) * height);
+    error_label_->setText(msg);
 }
 
 void
 LoginPage::showError(QLabel *label, const QString &msg)
 {
-        auto rect  = QFontMetrics(font()).boundingRect(msg);
-        int width  = rect.width();
-        int height = rect.height();
-        label->setFixedHeight((int)qCeil(width / 200.0) * height);
-        label->setText(msg);
+    auto rect  = QFontMetrics(font()).boundingRect(msg);
+    int width  = rect.width();
+    int height = rect.height();
+    label->setFixedHeight((int)qCeil(width / 200.0) * height);
+    label->setText(msg);
 }
 
 void
 LoginPage::onMatrixIdEntered()
 {
-        error_label_->setText("");
+    error_label_->setText("");
 
-        User user;
+    User user;
 
-        if (!matrixid_input_->isValid()) {
-                error_matrixid_label_->show();
-                showError(error_matrixid_label_,
-                          tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
-                return;
+    if (!matrixid_input_->isValid()) {
+        error_matrixid_label_->show();
+        showError(error_matrixid_label_,
+                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
+        return;
+    } else {
+        error_matrixid_label_->setText("");
+        error_matrixid_label_->hide();
+    }
+
+    try {
+        user = parse<User>(matrixid_input_->text().toStdString());
+    } catch (const std::exception &) {
+        showError(error_matrixid_label_,
+                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
+        return;
+    }
+
+    QString homeServer = QString::fromStdString(user.hostname());
+    if (homeServer != inferredServerAddress_) {
+        serverInput_->hide();
+        serverLayout_->removeWidget(errorIcon_);
+        errorIcon_->hide();
+        if (serverInput_->isVisible()) {
+            matrixidLayout_->removeWidget(spinner_);
+            serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
+            spinner_->start();
         } else {
-                error_matrixid_label_->setText("");
-                error_matrixid_label_->hide();
+            serverLayout_->removeWidget(spinner_);
+            matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
+            spinner_->start();
         }
 
-        try {
-                user = parse<User>(matrixid_input_->text().toStdString());
-        } catch (const std::exception &) {
-                showError(error_matrixid_label_,
-                          tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
-                return;
-        }
+        inferredServerAddress_ = homeServer;
+        serverInput_->setText(homeServer);
 
-        QString homeServer = QString::fromStdString(user.hostname());
-        if (homeServer != inferredServerAddress_) {
-                serverInput_->hide();
-                serverLayout_->removeWidget(errorIcon_);
-                errorIcon_->hide();
-                if (serverInput_->isVisible()) {
-                        matrixidLayout_->removeWidget(spinner_);
-                        serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
-                        spinner_->start();
-                } else {
-                        serverLayout_->removeWidget(spinner_);
-                        matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
-                        spinner_->start();
-                }
-
-                inferredServerAddress_ = homeServer;
-                serverInput_->setText(homeServer);
-
-                http::client()->set_server(user.hostname());
-                http::client()->verify_certificates(
-                  !UserSettings::instance()->disableCertificateValidation());
-
-                http::client()->well_known([this](const mtx::responses::WellKnown &res,
-                                                  mtx::http::RequestErr err) {
-                        if (err) {
-                                if (err->status_code == 404) {
-                                        nhlog::net()->info("Autodiscovery: No .well-known.");
-                                        checkHomeserverVersion();
-                                        return;
-                                }
-
-                                if (!err->parse_error.empty()) {
-                                        emit versionErrorCb(
-                                          tr("Autodiscovery failed. Received malformed response."));
-                                        nhlog::net()->error(
-                                          "Autodiscovery failed. Received malformed response.");
-                                        return;
-                                }
-
-                                emit versionErrorCb(tr("Autodiscovery failed. Unknown error when "
-                                                       "requesting .well-known."));
-                                nhlog::net()->error("Autodiscovery failed. Unknown error when "
-                                                    "requesting .well-known. {} {}",
-                                                    err->status_code,
-                                                    err->error_code);
-                                return;
-                        }
-
-                        nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url +
-                                           "'");
-                        http::client()->set_server(res.homeserver.base_url);
-                        checkHomeserverVersion();
-                });
-        }
+        http::client()->set_server(user.hostname());
+        http::client()->verify_certificates(
+          !UserSettings::instance()->disableCertificateValidation());
+
+        http::client()->well_known(
+          [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
+              if (err) {
+                  if (err->status_code == 404) {
+                      nhlog::net()->info("Autodiscovery: No .well-known.");
+                      checkHomeserverVersion();
+                      return;
+                  }
+
+                  if (!err->parse_error.empty()) {
+                      emit versionErrorCb(tr("Autodiscovery failed. Received malformed response."));
+                      nhlog::net()->error("Autodiscovery failed. Received malformed response.");
+                      return;
+                  }
+
+                  emit versionErrorCb(tr("Autodiscovery failed. Unknown error when "
+                                         "requesting .well-known."));
+                  nhlog::net()->error("Autodiscovery failed. Unknown error when "
+                                      "requesting .well-known. {} {}",
+                                      err->status_code,
+                                      err->error_code);
+                  return;
+              }
+
+              nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
+              http::client()->set_server(res.homeserver.base_url);
+              checkHomeserverVersion();
+          });
+    }
 }
 
 void
 LoginPage::checkHomeserverVersion()
 {
-        http::client()->versions(
-          [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
-                  if (err) {
-                          if (err->status_code == 404) {
-                                  emit versionErrorCb(tr("The required endpoints were not found. "
-                                                         "Possibly not a Matrix server."));
-                                  return;
-                          }
-
-                          if (!err->parse_error.empty()) {
-                                  emit versionErrorCb(tr("Received malformed response. Make sure "
-                                                         "the homeserver domain is valid."));
-                                  return;
-                          }
-
-                          emit versionErrorCb(tr(
-                            "An unknown error occured. Make sure the homeserver domain is valid."));
-                          return;
-                  }
+    http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+        if (err) {
+            if (err->status_code == 404) {
+                emit versionErrorCb(tr("The required endpoints were not found. "
+                                       "Possibly not a Matrix server."));
+                return;
+            }
 
-                  http::client()->get_login(
-                    [this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) {
-                            if (err || flows.flows.empty())
-                                    emit versionOkCb(true, false);
-
-                            bool ssoSupported_      = false;
-                            bool passwordSupported_ = false;
-                            for (const auto &flow : flows.flows) {
-                                    if (flow.type == mtx::user_interactive::auth_types::sso) {
-                                            ssoSupported_ = true;
-                                    } else if (flow.type ==
-                                               mtx::user_interactive::auth_types::password) {
-                                            passwordSupported_ = true;
-                                    }
-                            }
-                            emit versionOkCb(passwordSupported_, ssoSupported_);
-                    });
+            if (!err->parse_error.empty()) {
+                emit versionErrorCb(tr("Received malformed response. Make sure "
+                                       "the homeserver domain is valid."));
+                return;
+            }
+
+            emit versionErrorCb(
+              tr("An unknown error occured. Make sure the homeserver domain is valid."));
+            return;
+        }
+
+        http::client()->get_login(
+          [this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) {
+              if (err || flows.flows.empty())
+                  emit versionOkCb(true, false);
+
+              bool ssoSupported_      = false;
+              bool passwordSupported_ = false;
+              for (const auto &flow : flows.flows) {
+                  if (flow.type == mtx::user_interactive::auth_types::sso) {
+                      ssoSupported_ = true;
+                  } else if (flow.type == mtx::user_interactive::auth_types::password) {
+                      passwordSupported_ = true;
+                  }
+              }
+              emit versionOkCb(passwordSupported_, ssoSupported_);
           });
+    });
 }
 
 void
 LoginPage::onServerAddressEntered()
 {
-        error_label_->setText("");
-        http::client()->verify_certificates(
-          !UserSettings::instance()->disableCertificateValidation());
-        http::client()->set_server(serverInput_->text().toStdString());
-        checkHomeserverVersion();
-
-        serverLayout_->removeWidget(errorIcon_);
-        errorIcon_->hide();
-        serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
-        spinner_->start();
+    error_label_->setText("");
+    http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
+    http::client()->set_server(serverInput_->text().toStdString());
+    checkHomeserverVersion();
+
+    serverLayout_->removeWidget(errorIcon_);
+    errorIcon_->hide();
+    serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
+    spinner_->start();
 }
 
 void
 LoginPage::versionError(const QString &error)
 {
-        showError(error_label_, error);
-        serverInput_->show();
-
-        spinner_->stop();
-        serverLayout_->removeWidget(spinner_);
-        serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight);
-        errorIcon_->show();
-        matrixidLayout_->removeWidget(spinner_);
+    showError(error_label_, error);
+    serverInput_->show();
+
+    spinner_->stop();
+    serverLayout_->removeWidget(spinner_);
+    serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight);
+    errorIcon_->show();
+    matrixidLayout_->removeWidget(spinner_);
 }
 
 void
 LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_)
 {
-        passwordSupported = passwordSupported_;
-        ssoSupported      = ssoSupported_;
+    passwordSupported = passwordSupported_;
+    ssoSupported      = ssoSupported_;
 
-        serverLayout_->removeWidget(spinner_);
-        matrixidLayout_->removeWidget(spinner_);
-        spinner_->stop();
+    serverLayout_->removeWidget(spinner_);
+    matrixidLayout_->removeWidget(spinner_);
+    spinner_->stop();
 
-        sso_login_button_->setVisible(ssoSupported);
-        login_button_->setVisible(passwordSupported);
+    sso_login_button_->setVisible(ssoSupported);
+    login_button_->setVisible(passwordSupported);
 
-        if (serverInput_->isVisible())
-                serverInput_->hide();
+    if (serverInput_->isVisible())
+        serverInput_->hide();
 }
 
 void
 LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
 {
-        error_label_->setText("");
-        User user;
+    error_label_->setText("");
+    User user;
+
+    if (!matrixid_input_->isValid()) {
+        error_matrixid_label_->show();
+        showError(error_matrixid_label_,
+                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
+        return;
+    } else {
+        error_matrixid_label_->setText("");
+        error_matrixid_label_->hide();
+    }
+
+    try {
+        user = parse<User>(matrixid_input_->text().toStdString());
+    } catch (const std::exception &) {
+        showError(error_matrixid_label_,
+                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
+        return;
+    }
+
+    if (loginMethod == LoginMethod::Password) {
+        if (password_input_->text().isEmpty())
+            return showError(error_label_, tr("Empty password"));
+
+        http::client()->login(
+          user.localpart(),
+          password_input_->text().toStdString(),
+          deviceName_->text().trimmed().isEmpty() ? initialDeviceName()
+                                                  : deviceName_->text().toStdString(),
+          [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
+              if (err) {
+                  auto error = err->matrix_error.error;
+                  if (error.empty())
+                      error = err->parse_error;
+
+                  showErrorMessage(error_label_, QString::fromStdString(error));
+                  emit errorOccurred();
+                  return;
+              }
+
+              if (res.well_known) {
+                  http::client()->set_server(res.well_known->homeserver.base_url);
+                  nhlog::net()->info("Login requested to user server: " +
+                                     res.well_known->homeserver.base_url);
+              }
+
+              emit loginOk(res);
+          });
+    } else {
+        auto sso = new SSOHandler();
+        connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) {
+            mtx::requests::Login req{};
+            req.token     = token;
+            req.type      = mtx::user_interactive::auth_types::token;
+            req.device_id = deviceName_->text().trimmed().isEmpty()
+                              ? initialDeviceName()
+                              : deviceName_->text().toStdString();
+            http::client()->login(
+              req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
+                  if (err) {
+                      showErrorMessage(error_label_,
+                                       QString::fromStdString(err->matrix_error.error));
+                      emit errorOccurred();
+                      return;
+                  }
 
-        if (!matrixid_input_->isValid()) {
-                error_matrixid_label_->show();
-                showError(error_matrixid_label_,
-                          tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
-                return;
-        } else {
-                error_matrixid_label_->setText("");
-                error_matrixid_label_->hide();
-        }
+                  if (res.well_known) {
+                      http::client()->set_server(res.well_known->homeserver.base_url);
+                      nhlog::net()->info("Login requested to user server: " +
+                                         res.well_known->homeserver.base_url);
+                  }
 
-        try {
-                user = parse<User>(matrixid_input_->text().toStdString());
-        } catch (const std::exception &) {
-                showError(error_matrixid_label_,
-                          tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
-                return;
-        }
+                  emit loginOk(res);
+              });
+            sso->deleteLater();
+        });
+        connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
+            showErrorMessage(error_label_, tr("SSO login failed"));
+            emit errorOccurred();
+            sso->deleteLater();
+        });
 
-        if (loginMethod == LoginMethod::Password) {
-                if (password_input_->text().isEmpty())
-                        return showError(error_label_, tr("Empty password"));
-
-                http::client()->login(
-                  user.localpart(),
-                  password_input_->text().toStdString(),
-                  deviceName_->text().trimmed().isEmpty() ? initialDeviceName()
-                                                          : deviceName_->text().toStdString(),
-                  [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
-                          if (err) {
-                                  auto error = err->matrix_error.error;
-                                  if (error.empty())
-                                          error = err->parse_error;
-
-                                  showErrorMessage(error_label_, QString::fromStdString(error));
-                                  emit errorOccurred();
-                                  return;
-                          }
-
-                          if (res.well_known) {
-                                  http::client()->set_server(res.well_known->homeserver.base_url);
-                                  nhlog::net()->info("Login requested to user server: " +
-                                                     res.well_known->homeserver.base_url);
-                          }
-
-                          emit loginOk(res);
-                  });
-        } else {
-                auto sso = new SSOHandler();
-                connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) {
-                        mtx::requests::Login req{};
-                        req.token     = token;
-                        req.type      = mtx::user_interactive::auth_types::token;
-                        req.device_id = deviceName_->text().trimmed().isEmpty()
-                                          ? initialDeviceName()
-                                          : deviceName_->text().toStdString();
-                        http::client()->login(
-                          req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
-                                  if (err) {
-                                          showErrorMessage(
-                                            error_label_,
-                                            QString::fromStdString(err->matrix_error.error));
-                                          emit errorOccurred();
-                                          return;
-                                  }
-
-                                  if (res.well_known) {
-                                          http::client()->set_server(
-                                            res.well_known->homeserver.base_url);
-                                          nhlog::net()->info("Login requested to user server: " +
-                                                             res.well_known->homeserver.base_url);
-                                  }
-
-                                  emit loginOk(res);
-                          });
-                        sso->deleteLater();
-                });
-                connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
-                        showErrorMessage(error_label_, tr("SSO login failed"));
-                        emit errorOccurred();
-                        sso->deleteLater();
-                });
-
-                QDesktopServices::openUrl(
-                  QString::fromStdString(http::client()->login_sso_redirect(sso->url())));
-        }
+        QDesktopServices::openUrl(
+          QString::fromStdString(http::client()->login_sso_redirect(sso->url())));
+    }
 
-        emit loggingIn();
+    emit loggingIn();
 }
 
 void
 LoginPage::reset()
 {
-        matrixid_input_->clear();
-        password_input_->clear();
-        password_input_->show();
-        serverInput_->clear();
-
-        spinner_->stop();
-        errorIcon_->hide();
-        serverLayout_->removeWidget(spinner_);
-        serverLayout_->removeWidget(errorIcon_);
-        matrixidLayout_->removeWidget(spinner_);
-
-        inferredServerAddress_.clear();
+    matrixid_input_->clear();
+    password_input_->clear();
+    password_input_->show();
+    serverInput_->clear();
+
+    spinner_->stop();
+    errorIcon_->hide();
+    serverLayout_->removeWidget(spinner_);
+    serverLayout_->removeWidget(errorIcon_);
+    matrixidLayout_->removeWidget(spinner_);
+
+    inferredServerAddress_.clear();
 }
 
 void
 LoginPage::onBackButtonClicked()
 {
-        emit backButtonClicked();
+    emit backButtonClicked();
 }
 
 void
 LoginPage::paintEvent(QPaintEvent *)
 {
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+    QStyleOption opt;
+    opt.init(this);
+    QPainter p(this);
+    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
 }
diff --git a/src/LoginPage.h b/src/LoginPage.h
index 2e1eb9b95869d25aa53942bdf5950fc339e26e36..01dd27e13b43d63f73ff954721a7a2b3677fe6e5 100644
--- a/src/LoginPage.h
+++ b/src/LoginPage.h
@@ -24,101 +24,101 @@ struct Login;
 
 class LoginPage : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        enum class LoginMethod
-        {
-                Password,
-                SSO,
-        };
+    enum class LoginMethod
+    {
+        Password,
+        SSO,
+    };
 
-        LoginPage(QWidget *parent = nullptr);
+    LoginPage(QWidget *parent = nullptr);
 
-        void reset();
+    void reset();
 
 signals:
-        void backButtonClicked();
-        void loggingIn();
-        void errorOccurred();
+    void backButtonClicked();
+    void loggingIn();
+    void errorOccurred();
 
-        //! Used to trigger the corresponding slot outside of the main thread.
-        void versionErrorCb(const QString &err);
-        void versionOkCb(bool passwordSupported, bool ssoSupported);
+    //! Used to trigger the corresponding slot outside of the main thread.
+    void versionErrorCb(const QString &err);
+    void versionOkCb(bool passwordSupported, bool ssoSupported);
 
-        void loginOk(const mtx::responses::Login &res);
-        void showErrorMessage(QLabel *label, const QString &msg);
+    void loginOk(const mtx::responses::Login &res);
+    void showErrorMessage(QLabel *label, const QString &msg);
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 public slots:
-        // Displays errors produced during the login.
-        void showError(const QString &msg);
-        void showError(QLabel *label, const QString &msg);
+    // Displays errors produced during the login.
+    void showError(const QString &msg);
+    void showError(QLabel *label, const QString &msg);
 
 private slots:
-        // Callback for the back button.
-        void onBackButtonClicked();
+    // Callback for the back button.
+    void onBackButtonClicked();
 
-        // Callback for the login button.
-        void onLoginButtonClicked(LoginMethod loginMethod);
+    // Callback for the login button.
+    void onLoginButtonClicked(LoginMethod loginMethod);
 
-        // Callback for probing the server found in the mxid
-        void onMatrixIdEntered();
+    // Callback for probing the server found in the mxid
+    void onMatrixIdEntered();
 
-        // Callback for probing the manually entered server
-        void onServerAddressEntered();
+    // Callback for probing the manually entered server
+    void onServerAddressEntered();
 
-        // Callback for errors produced during server probing
-        void versionError(const QString &error_message);
-        // Callback for successful server probing
-        void versionOk(bool passwordSupported, bool ssoSupported);
+    // Callback for errors produced during server probing
+    void versionError(const QString &error_message);
+    // Callback for successful server probing
+    void versionOk(bool passwordSupported, bool ssoSupported);
 
 private:
-        void checkHomeserverVersion();
-        std::string initialDeviceName()
-        {
+    void checkHomeserverVersion();
+    std::string initialDeviceName()
+    {
 #if defined(Q_OS_MAC)
-                return "Nheko on macOS";
+        return "Nheko on macOS";
 #elif defined(Q_OS_LINUX)
-                return "Nheko on Linux";
+        return "Nheko on Linux";
 #elif defined(Q_OS_WIN)
-                return "Nheko on Windows";
+        return "Nheko on Windows";
 #elif defined(Q_OS_FREEBSD)
-                return "Nheko on FreeBSD";
+        return "Nheko on FreeBSD";
 #else
-                return "Nheko";
+        return "Nheko";
 #endif
-        }
+    }
 
-        QVBoxLayout *top_layout_;
+    QVBoxLayout *top_layout_;
 
-        QHBoxLayout *top_bar_layout_;
-        QHBoxLayout *logo_layout_;
-        QHBoxLayout *button_layout_;
+    QHBoxLayout *top_bar_layout_;
+    QHBoxLayout *logo_layout_;
+    QHBoxLayout *button_layout_;
 
-        QLabel *logo_;
-        QLabel *error_label_;
-        QLabel *error_matrixid_label_;
+    QLabel *logo_;
+    QLabel *error_label_;
+    QLabel *error_matrixid_label_;
 
-        QHBoxLayout *serverLayout_;
-        QHBoxLayout *matrixidLayout_;
-        LoadingIndicator *spinner_;
-        QLabel *errorIcon_;
-        QString inferredServerAddress_;
+    QHBoxLayout *serverLayout_;
+    QHBoxLayout *matrixidLayout_;
+    LoadingIndicator *spinner_;
+    QLabel *errorIcon_;
+    QString inferredServerAddress_;
 
-        FlatButton *back_button_;
-        RaisedButton *login_button_, *sso_login_button_;
+    FlatButton *back_button_;
+    RaisedButton *login_button_, *sso_login_button_;
 
-        QWidget *form_widget_;
-        QHBoxLayout *form_wrapper_;
-        QVBoxLayout *form_layout_;
+    QWidget *form_widget_;
+    QHBoxLayout *form_wrapper_;
+    QVBoxLayout *form_layout_;
 
-        TextField *matrixid_input_;
-        TextField *password_input_;
-        TextField *deviceName_;
-        TextField *serverInput_;
-        bool passwordSupported = true;
-        bool ssoSupported      = false;
+    TextField *matrixid_input_;
+    TextField *password_input_;
+    TextField *deviceName_;
+    TextField *serverInput_;
+    bool passwordSupported = true;
+    bool ssoSupported      = false;
 };
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 8bc90f29eae3319cb1b023648eab3dcd793408e0..34db0d1d7ad52657b3d5d062b2bf2a1a13d08132 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -7,7 +7,6 @@
 #include <QLayout>
 #include <QMessageBox>
 #include <QPluginLoader>
-#include <QSettings>
 #include <QShortcut>
 
 #include <mtx/requests.hpp>
@@ -17,6 +16,7 @@
 #include "Cache_p.h"
 #include "ChatPage.h"
 #include "Config.h"
+#include "JdenticonProvider.h"
 #include "Logging.h"
 #include "LoginPage.h"
 #include "MainWindow.h"
@@ -26,16 +26,13 @@
 #include "TrayIcon.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
-#include "WebRTCSession.h"
 #include "WelcomePage.h"
 #include "ui/LoadingIndicator.h"
 #include "ui/OverlayModal.h"
 #include "ui/SnackBar.h"
+#include "voip/WebRTCSession.h"
 
 #include "dialogs/CreateRoom.h"
-#include "dialogs/JoinRoom.h"
-#include "dialogs/LeaveRoom.h"
-#include "dialogs/Logout.h"
 
 MainWindow *MainWindow::instance_ = nullptr;
 
@@ -43,438 +40,366 @@ MainWindow::MainWindow(QWidget *parent)
   : QMainWindow(parent)
   , userSettings_{UserSettings::instance()}
 {
-        instance_ = this;
-
-        setWindowTitle(0);
-        setObjectName("MainWindow");
-
-        modal_ = new OverlayModal(this);
-
-        restoreWindowSize();
-
-        QFont font;
-        font.setStyleStrategy(QFont::PreferAntialias);
-        setFont(font);
-
-        trayIcon_ = new TrayIcon(":/logos/nheko.svg", this);
-
-        welcome_page_     = new WelcomePage(this);
-        login_page_       = new LoginPage(this);
-        register_page_    = new RegisterPage(this);
-        chat_page_        = new ChatPage(userSettings_, this);
-        userSettingsPage_ = new UserSettingsPage(userSettings_, this);
-
-        // Initialize sliding widget manager.
-        pageStack_ = new QStackedWidget(this);
-        pageStack_->addWidget(welcome_page_);
-        pageStack_->addWidget(login_page_);
-        pageStack_->addWidget(register_page_);
-        pageStack_->addWidget(chat_page_);
-        pageStack_->addWidget(userSettingsPage_);
-
-        setCentralWidget(pageStack_);
-
-        connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage()));
-        connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage()));
-
-        connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
-        connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar);
-        connect(
-          register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar);
-        connect(
-          login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
-        connect(register_page_, &RegisterPage::errorOccurred, this, [this]() {
-                removeOverlayProgressBar();
-        });
-        connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
-
-        connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage);
-        connect(
-          chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar);
-        connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle);
-        connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int)));
-        connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) {
-                login_page_->showError(msg);
-                showLoginPage();
-        });
-
-        connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() {
-                pageStack_->setCurrentWidget(chat_page_);
-        });
+    instance_ = this;
+
+    setWindowTitle(0);
+    setObjectName("MainWindow");
+
+    modal_ = new OverlayModal(this);
+
+    restoreWindowSize();
+
+    QFont font;
+    font.setStyleStrategy(QFont::PreferAntialias);
+    setFont(font);
+
+    trayIcon_ = new TrayIcon(":/logos/nheko.svg", this);
+
+    welcome_page_     = new WelcomePage(this);
+    login_page_       = new LoginPage(this);
+    register_page_    = new RegisterPage(this);
+    chat_page_        = new ChatPage(userSettings_, this);
+    userSettingsPage_ = new UserSettingsPage(userSettings_, this);
+
+    // Initialize sliding widget manager.
+    pageStack_ = new QStackedWidget(this);
+    pageStack_->addWidget(welcome_page_);
+    pageStack_->addWidget(login_page_);
+    pageStack_->addWidget(register_page_);
+    pageStack_->addWidget(chat_page_);
+    pageStack_->addWidget(userSettingsPage_);
+
+    setCentralWidget(pageStack_);
+
+    connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage()));
+    connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage()));
+
+    connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
+    connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar);
+    connect(register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar);
+    connect(login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
+    connect(
+      register_page_, &RegisterPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
+    connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
+
+    connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage);
+    connect(
+      chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar);
+    connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle);
+    connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int)));
+    connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) {
+        login_page_->showError(msg);
+        showLoginPage();
+    });
+
+    connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() {
+        pageStack_->setCurrentWidget(chat_page_);
+    });
 
-        connect(
-          userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
-        connect(
-          userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
-        connect(trayIcon_,
-                SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
-                this,
-                SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
+    connect(userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
+    connect(
+      userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
+    connect(trayIcon_,
+            SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
+            this,
+            SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
 
-        connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar()));
+    connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar()));
 
-        connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged);
+    connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged);
 
-        connect(
-          chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
+    connect(chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
 
-        connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
-                http::client()->set_user(res.user_id);
-                showChatPage();
-        });
+    connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
+        http::client()->set_user(res.user_id);
+        showChatPage();
+    });
 
-        connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
+    connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
 
-        QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
-        connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
+    QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
+    connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
 
-        trayIcon_->setVisible(userSettings_->tray());
+    trayIcon_->setVisible(userSettings_->tray());
 
+    // load cache on event loop
+    QTimer::singleShot(0, this, [this] {
         if (hasActiveUser()) {
-                QString token       = userSettings_->accessToken();
-                QString home_server = userSettings_->homeserver();
-                QString user_id     = userSettings_->userId();
-                QString device_id   = userSettings_->deviceId();
-
-                http::client()->set_access_token(token.toStdString());
-                http::client()->set_server(home_server.toStdString());
-                http::client()->set_device_id(device_id.toStdString());
-
-                try {
-                        using namespace mtx::identifiers;
-                        http::client()->set_user(parse<User>(user_id.toStdString()));
-                } catch (const std::invalid_argument &) {
-                        nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
-                                              user_id.toStdString());
-                }
-
-                showChatPage();
-        }
-
-        if (loadJdenticonPlugin()) {
-                nhlog::ui()->info("loaded jdenticon.");
+            QString token       = userSettings_->accessToken();
+            QString home_server = userSettings_->homeserver();
+            QString user_id     = userSettings_->userId();
+            QString device_id   = userSettings_->deviceId();
+
+            http::client()->set_access_token(token.toStdString());
+            http::client()->set_server(home_server.toStdString());
+            http::client()->set_device_id(device_id.toStdString());
+
+            try {
+                using namespace mtx::identifiers;
+                http::client()->set_user(parse<User>(user_id.toStdString()));
+            } catch (const std::invalid_argument &) {
+                nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
+                                      user_id.toStdString());
+            }
+
+            showChatPage();
         }
+    });
 }
 
 void
 MainWindow::setWindowTitle(int notificationCount)
 {
-        QString name = "nheko";
-
-        if (!userSettings_.data()->profile().isEmpty())
-                name += " | " + userSettings_.data()->profile();
-        if (notificationCount > 0) {
-                name.append(QString{" (%1)"}.arg(notificationCount));
-        }
-        QMainWindow::setWindowTitle(name);
+    QString name = "nheko";
+
+    if (!userSettings_.data()->profile().isEmpty())
+        name += " | " + userSettings_.data()->profile();
+    if (notificationCount > 0) {
+        name.append(QString{" (%1)"}.arg(notificationCount));
+    }
+    QMainWindow::setWindowTitle(name);
 }
 
 bool
 MainWindow::event(QEvent *event)
 {
-        auto type = event->type();
-        if (type == QEvent::WindowActivate) {
-                emit focusChanged(true);
-        } else if (type == QEvent::WindowDeactivate) {
-                emit focusChanged(false);
-        }
-
-        return QMainWindow::event(event);
+    auto type = event->type();
+    if (type == QEvent::WindowActivate) {
+        emit focusChanged(true);
+    } else if (type == QEvent::WindowDeactivate) {
+        emit focusChanged(false);
+    }
+
+    return QMainWindow::event(event);
 }
 
 void
 MainWindow::restoreWindowSize()
 {
-        QSettings settings;
-        int savedWidth  = settings.value("window/width").toInt();
-        int savedheight = settings.value("window/height").toInt();
-
-        if (savedWidth == 0 || savedheight == 0)
-                resize(conf::window::width, conf::window::height);
-        else
-                resize(savedWidth, savedheight);
+    int savedWidth  = userSettings_->qsettings()->value("window/width").toInt();
+    int savedheight = userSettings_->qsettings()->value("window/height").toInt();
+
+    nhlog::ui()->info("Restoring window size {}x{}", savedWidth, savedheight);
+
+    if (savedWidth == 0 || savedheight == 0)
+        resize(conf::window::width, conf::window::height);
+    else
+        resize(savedWidth, savedheight);
 }
 
 void
 MainWindow::saveCurrentWindowSize()
 {
-        QSettings settings;
-        QSize current = size();
+    auto settings = userSettings_->qsettings();
+    QSize current = size();
 
-        settings.setValue("window/width", current.width());
-        settings.setValue("window/height", current.height());
+    settings->setValue("window/width", current.width());
+    settings->setValue("window/height", current.height());
 }
 
 void
 MainWindow::removeOverlayProgressBar()
 {
-        QTimer *timer = new QTimer(this);
-        timer->setSingleShot(true);
+    QTimer *timer = new QTimer(this);
+    timer->setSingleShot(true);
 
-        connect(timer, &QTimer::timeout, [this, timer]() {
-                timer->deleteLater();
+    connect(timer, &QTimer::timeout, [this, timer]() {
+        timer->deleteLater();
 
-                if (modal_)
-                        modal_->hide();
+        if (modal_)
+            modal_->hide();
 
-                if (spinner_)
-                        spinner_->stop();
-        });
+        if (spinner_)
+            spinner_->stop();
+    });
 
-        // FIXME:  Snackbar doesn't work if it's initialized in the constructor.
-        QTimer::singleShot(0, this, [this]() {
-                snackBar_ = new SnackBar(this);
-                connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage);
-        });
+    // FIXME:  Snackbar doesn't work if it's initialized in the constructor.
+    QTimer::singleShot(0, this, [this]() {
+        snackBar_ = new SnackBar(this);
+        connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage);
+    });
 
-        timer->start(50);
+    timer->start(50);
 }
 
 void
 MainWindow::showChatPage()
 {
-        auto userid     = QString::fromStdString(http::client()->user_id().to_string());
-        auto device_id  = QString::fromStdString(http::client()->device_id());
-        auto homeserver = QString::fromStdString(http::client()->server() + ":" +
-                                                 std::to_string(http::client()->port()));
-        auto token      = QString::fromStdString(http::client()->access_token());
-
-        userSettings_.data()->setUserId(userid);
-        userSettings_.data()->setAccessToken(token);
-        userSettings_.data()->setDeviceId(device_id);
-        userSettings_.data()->setHomeserver(homeserver);
-
-        showOverlayProgressBar();
-
-        pageStack_->setCurrentWidget(chat_page_);
-
-        pageStack_->removeWidget(welcome_page_);
-        pageStack_->removeWidget(login_page_);
-        pageStack_->removeWidget(register_page_);
-
-        login_page_->reset();
-        chat_page_->bootstrap(userid, homeserver, token);
-        connect(cache::client(),
-                &Cache::secretChanged,
-                userSettingsPage_,
-                &UserSettingsPage::updateSecretStatus);
-        emit reload();
+    auto userid     = QString::fromStdString(http::client()->user_id().to_string());
+    auto device_id  = QString::fromStdString(http::client()->device_id());
+    auto homeserver = QString::fromStdString(http::client()->server() + ":" +
+                                             std::to_string(http::client()->port()));
+    auto token      = QString::fromStdString(http::client()->access_token());
+
+    userSettings_.data()->setUserId(userid);
+    userSettings_.data()->setAccessToken(token);
+    userSettings_.data()->setDeviceId(device_id);
+    userSettings_.data()->setHomeserver(homeserver);
+
+    showOverlayProgressBar();
+
+    pageStack_->setCurrentWidget(chat_page_);
+
+    pageStack_->removeWidget(welcome_page_);
+    pageStack_->removeWidget(login_page_);
+    pageStack_->removeWidget(register_page_);
+
+    login_page_->reset();
+    chat_page_->bootstrap(userid, homeserver, token);
+    connect(cache::client(),
+            &Cache::secretChanged,
+            userSettingsPage_,
+            &UserSettingsPage::updateSecretStatus);
+    emit reload();
 }
 
 void
 MainWindow::closeEvent(QCloseEvent *event)
 {
-        if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) {
-                if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") !=
-                    QMessageBox::Yes) {
-                        event->ignore();
-                        return;
-                }
+    if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) {
+        if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") !=
+            QMessageBox::Yes) {
+            event->ignore();
+            return;
         }
+    }
 
-        if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() &&
-            userSettings_->tray()) {
-                event->ignore();
-                hide();
-        }
+    if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && userSettings_->tray()) {
+        event->ignore();
+        hide();
+    }
 }
 
 void
 MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
 {
-        switch (reason) {
-        case QSystemTrayIcon::Trigger:
-                if (!isVisible()) {
-                        show();
-                } else {
-                        hide();
-                }
-                break;
-        default:
-                break;
+    switch (reason) {
+    case QSystemTrayIcon::Trigger:
+        if (!isVisible()) {
+            show();
+        } else {
+            hide();
         }
+        break;
+    default:
+        break;
+    }
 }
 
 bool
 MainWindow::hasActiveUser()
 {
-        QSettings settings;
-        QString prefix;
-        if (userSettings_->profile() != "")
-                prefix = "profile/" + userSettings_->profile() + "/";
-
-        return settings.contains(prefix + "auth/access_token") &&
-               settings.contains(prefix + "auth/home_server") &&
-               settings.contains(prefix + "auth/user_id");
-}
-
-void
-MainWindow::openLeaveRoomDialog(const QString &room_id)
-{
-        auto dialog = new dialogs::LeaveRoom(this);
-        connect(dialog, &dialogs::LeaveRoom::leaving, this, [this, room_id]() {
-                chat_page_->leaveRoom(room_id);
-        });
-
-        showDialog(dialog);
+    auto settings = userSettings_->qsettings();
+    QString prefix;
+    if (userSettings_->profile() != "")
+        prefix = "profile/" + userSettings_->profile() + "/";
+
+    return settings->contains(prefix + "auth/access_token") &&
+           settings->contains(prefix + "auth/home_server") &&
+           settings->contains(prefix + "auth/user_id");
 }
 
 void
 MainWindow::showOverlayProgressBar()
 {
-        spinner_ = new LoadingIndicator(this);
-        spinner_->setFixedHeight(100);
-        spinner_->setFixedWidth(100);
-        spinner_->setObjectName("ChatPageLoadSpinner");
-        spinner_->start();
-
-        showSolidOverlayModal(spinner_);
-}
-
-void
-MainWindow::openJoinRoomDialog(std::function<void(const QString &room_id)> callback)
-{
-        auto dialog = new dialogs::JoinRoom(this);
-        connect(dialog, &dialogs::JoinRoom::joinRoom, this, [callback](const QString &room) {
-                if (!room.isEmpty())
-                        callback(room);
-        });
+    spinner_ = new LoadingIndicator(this);
+    spinner_->setFixedHeight(100);
+    spinner_->setFixedWidth(100);
+    spinner_->setObjectName("ChatPageLoadSpinner");
+    spinner_->start();
 
-        showDialog(dialog);
+    showSolidOverlayModal(spinner_);
 }
 
 void
 MainWindow::openCreateRoomDialog(
   std::function<void(const mtx::requests::CreateRoom &request)> callback)
 {
-        auto dialog = new dialogs::CreateRoom(this);
-        connect(dialog,
-                &dialogs::CreateRoom::createRoom,
-                this,
-                [callback](const mtx::requests::CreateRoom &request) { callback(request); });
+    auto dialog = new dialogs::CreateRoom(this);
+    connect(dialog,
+            &dialogs::CreateRoom::createRoom,
+            this,
+            [callback](const mtx::requests::CreateRoom &request) { callback(request); });
 
-        showDialog(dialog);
+    showDialog(dialog);
 }
 
 void
 MainWindow::showTransparentOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
 {
-        modal_->setWidget(content);
-        modal_->setColor(QColor(30, 30, 30, 150));
-        modal_->setDismissible(true);
-        modal_->setContentAlignment(flags);
-        modal_->raise();
-        modal_->show();
+    modal_->setWidget(content);
+    modal_->setColor(QColor(30, 30, 30, 150));
+    modal_->setDismissible(true);
+    modal_->setContentAlignment(flags);
+    modal_->raise();
+    modal_->show();
 }
 
 void
 MainWindow::showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
 {
-        modal_->setWidget(content);
-        modal_->setColor(QColor(30, 30, 30));
-        modal_->setDismissible(false);
-        modal_->setContentAlignment(flags);
-        modal_->raise();
-        modal_->show();
-}
-
-void
-MainWindow::openLogoutDialog()
-{
-        auto dialog = new dialogs::Logout(this);
-        connect(dialog, &dialogs::Logout::loggingOut, this, [this]() {
-                if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) {
-                        if (QMessageBox::question(
-                              this, "nheko", "A call is in progress. Log out?") !=
-                            QMessageBox::Yes) {
-                                return;
-                        }
-                        WebRTCSession::instance().end();
-                }
-                chat_page_->initiateLogout();
-        });
-
-        showDialog(dialog);
+    modal_->setWidget(content);
+    modal_->setColor(QColor(30, 30, 30));
+    modal_->setDismissible(false);
+    modal_->setContentAlignment(flags);
+    modal_->raise();
+    modal_->show();
 }
 
 bool
 MainWindow::hasActiveDialogs() const
 {
-        return !modal_ && modal_->isVisible();
+    return !modal_ && modal_->isVisible();
 }
 
 bool
 MainWindow::pageSupportsTray() const
 {
-        return !welcome_page_->isVisible() && !login_page_->isVisible() &&
-               !register_page_->isVisible();
+    return !welcome_page_->isVisible() && !login_page_->isVisible() && !register_page_->isVisible();
 }
 
 void
 MainWindow::hideOverlay()
 {
-        if (modal_)
-                modal_->hide();
+    if (modal_)
+        modal_->hide();
 }
 
 inline void
 MainWindow::showDialog(QWidget *dialog)
 {
-        utils::centerWidget(dialog, this);
-        dialog->raise();
-        dialog->show();
+    utils::centerWidget(dialog, this);
+    dialog->raise();
+    dialog->show();
 }
 
-bool
-MainWindow::loadJdenticonPlugin()
-{
-        QDir pluginsDir(qApp->applicationDirPath());
-
-        bool plugins = pluginsDir.cd("plugins");
-        if (plugins) {
-                foreach (QString fileName, pluginsDir.entryList(QDir::Files)) {
-                        QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
-                        QObject *plugin = pluginLoader.instance();
-                        if (plugin) {
-                                jdenticonInteface_ = qobject_cast<JdenticonInterface *>(plugin);
-                                if (jdenticonInteface_) {
-                                        nhlog::ui()->info("Found jdenticon plugin.");
-                                        return true;
-                                }
-                        }
-                }
-        }
-
-        nhlog::ui()->info("jdenticon plugin not found.");
-        return false;
-}
 void
 MainWindow::showWelcomePage()
 {
-        removeOverlayProgressBar();
-        pageStack_->addWidget(welcome_page_);
-        pageStack_->setCurrentWidget(welcome_page_);
+    removeOverlayProgressBar();
+    pageStack_->addWidget(welcome_page_);
+    pageStack_->setCurrentWidget(welcome_page_);
 }
 
 void
 MainWindow::showLoginPage()
 {
-        if (modal_)
-                modal_->hide();
+    if (modal_)
+        modal_->hide();
 
-        pageStack_->addWidget(login_page_);
-        pageStack_->setCurrentWidget(login_page_);
+    pageStack_->addWidget(login_page_);
+    pageStack_->setCurrentWidget(login_page_);
 }
 
 void
 MainWindow::showRegisterPage()
 {
-        pageStack_->addWidget(register_page_);
-        pageStack_->setCurrentWidget(register_page_);
+    pageStack_->addWidget(register_page_);
+    pageStack_->setCurrentWidget(register_page_);
 }
 
 void
 MainWindow::showUserSettingsPage()
 {
-        pageStack_->setCurrentWidget(userSettingsPage_);
+    pageStack_->setCurrentWidget(userSettingsPage_);
 }
diff --git a/src/MainWindow.h b/src/MainWindow.h
index d423af9fd008d61264e7dce28aafd731e946ce11..b9d2fe5fbc503a03846a17b7ff82d411b88be96b 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -37,8 +37,6 @@ struct CreateRoom;
 namespace dialogs {
 class CreateRoom;
 class InviteUsers;
-class JoinRoom;
-class LeaveRoom;
 class Logout;
 class MemberList;
 class ReCaptcha;
@@ -46,97 +44,90 @@ class ReCaptcha;
 
 class MainWindow : public QMainWindow
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(int x READ x CONSTANT)
-        Q_PROPERTY(int y READ y CONSTANT)
-        Q_PROPERTY(int width READ width CONSTANT)
-        Q_PROPERTY(int height READ height CONSTANT)
+    Q_PROPERTY(int x READ x CONSTANT)
+    Q_PROPERTY(int y READ y CONSTANT)
+    Q_PROPERTY(int width READ width CONSTANT)
+    Q_PROPERTY(int height READ height CONSTANT)
 
 public:
-        explicit MainWindow(QWidget *parent = nullptr);
+    explicit MainWindow(QWidget *parent = nullptr);
 
-        static MainWindow *instance() { return instance_; }
-        void saveCurrentWindowSize();
+    static MainWindow *instance() { return instance_; }
+    void saveCurrentWindowSize();
 
-        void openLeaveRoomDialog(const QString &room_id);
-        void openInviteUsersDialog(std::function<void(const QStringList &invitees)> callback);
-        void openCreateRoomDialog(
-          std::function<void(const mtx::requests::CreateRoom &request)> callback);
-        void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
-        void openLogoutDialog();
+    void openCreateRoomDialog(
+      std::function<void(const mtx::requests::CreateRoom &request)> callback);
+    void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
+    void openLogoutDialog();
 
-        void hideOverlay();
-        void showSolidOverlayModal(QWidget *content,
-                                   QFlags<Qt::AlignmentFlag> flags = Qt::AlignCenter);
-        void showTransparentOverlayModal(QWidget *content,
-                                         QFlags<Qt::AlignmentFlag> flags = Qt::AlignTop |
-                                                                           Qt::AlignHCenter);
+    void hideOverlay();
+    void showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags = Qt::AlignCenter);
+    void showTransparentOverlayModal(QWidget *content,
+                                     QFlags<Qt::AlignmentFlag> flags = Qt::AlignTop |
+                                                                       Qt::AlignHCenter);
 
 protected:
-        void closeEvent(QCloseEvent *event) override;
-        bool event(QEvent *event) override;
+    void closeEvent(QCloseEvent *event) override;
+    bool event(QEvent *event) override;
 
 private slots:
-        //! Handle interaction with the tray icon.
-        void iconActivated(QSystemTrayIcon::ActivationReason reason);
+    //! Handle interaction with the tray icon.
+    void iconActivated(QSystemTrayIcon::ActivationReason reason);
 
-        //! Show the welcome page in the main window.
-        void showWelcomePage();
+    //! Show the welcome page in the main window.
+    void showWelcomePage();
 
-        //! Show the login page in the main window.
-        void showLoginPage();
+    //! Show the login page in the main window.
+    void showLoginPage();
 
-        //! Show the register page in the main window.
-        void showRegisterPage();
+    //! Show the register page in the main window.
+    void showRegisterPage();
 
-        //! Show user settings page.
-        void showUserSettingsPage();
+    //! Show user settings page.
+    void showUserSettingsPage();
 
-        //! Show the chat page and start communicating with the given access token.
-        void showChatPage();
+    //! Show the chat page and start communicating with the given access token.
+    void showChatPage();
 
-        void showOverlayProgressBar();
-        void removeOverlayProgressBar();
+    void showOverlayProgressBar();
+    void removeOverlayProgressBar();
 
-        virtual void setWindowTitle(int notificationCount);
+    virtual void setWindowTitle(int notificationCount);
 
 signals:
-        void focusChanged(const bool focused);
-        void reload();
+    void focusChanged(const bool focused);
+    void reload();
 
 private:
-        bool loadJdenticonPlugin();
-
-        void showDialog(QWidget *dialog);
-        bool hasActiveUser();
-        void restoreWindowSize();
-        //! Check if there is an open dialog.
-        bool hasActiveDialogs() const;
-        //! Check if the current page supports the "minimize to tray" functionality.
-        bool pageSupportsTray() const;
-
-        static MainWindow *instance_;
-
-        //! The initial welcome screen.
-        WelcomePage *welcome_page_;
-        //! The login screen.
-        LoginPage *login_page_;
-        //! The register page.
-        RegisterPage *register_page_;
-        //! A stacked widget that handles the transitions between widgets.
-        QStackedWidget *pageStack_;
-        //! The main chat area.
-        ChatPage *chat_page_;
-        UserSettingsPage *userSettingsPage_;
-        QSharedPointer<UserSettings> userSettings_;
-        //! Tray icon that shows the unread message count.
-        TrayIcon *trayIcon_;
-        //! Notifications display.
-        SnackBar *snackBar_ = nullptr;
-        //! Overlay modal used to project other widgets.
-        OverlayModal *modal_       = nullptr;
-        LoadingIndicator *spinner_ = nullptr;
-
-        JdenticonInterface *jdenticonInteface_ = nullptr;
+    void showDialog(QWidget *dialog);
+    bool hasActiveUser();
+    void restoreWindowSize();
+    //! Check if there is an open dialog.
+    bool hasActiveDialogs() const;
+    //! Check if the current page supports the "minimize to tray" functionality.
+    bool pageSupportsTray() const;
+
+    static MainWindow *instance_;
+
+    //! The initial welcome screen.
+    WelcomePage *welcome_page_;
+    //! The login screen.
+    LoginPage *login_page_;
+    //! The register page.
+    RegisterPage *register_page_;
+    //! A stacked widget that handles the transitions between widgets.
+    QStackedWidget *pageStack_;
+    //! The main chat area.
+    ChatPage *chat_page_;
+    UserSettingsPage *userSettingsPage_;
+    QSharedPointer<UserSettings> userSettings_;
+    //! Tray icon that shows the unread message count.
+    TrayIcon *trayIcon_;
+    //! Notifications display.
+    SnackBar *snackBar_ = nullptr;
+    //! Overlay modal used to project other widgets.
+    OverlayModal *modal_       = nullptr;
+    LoadingIndicator *spinner_ = nullptr;
 };
diff --git a/src/MatrixClient.cpp b/src/MatrixClient.cpp
index 196a93224c156528b6a2843efc44b0924eb382f9..2ceb53a8ed53e74cf1ab180e9aef34ac7651bf8f 100644
--- a/src/MatrixClient.cpp
+++ b/src/MatrixClient.cpp
@@ -37,31 +37,31 @@ namespace http {
 mtx::http::Client *
 client()
 {
-        return client_.get();
+    return client_.get();
 }
 
 bool
 is_logged_in()
 {
-        return !client_->access_token().empty();
+    return !client_->access_token().empty();
 }
 
 void
 init()
 {
-        qRegisterMetaType<mtx::responses::Login>();
-        qRegisterMetaType<mtx::responses::Messages>();
-        qRegisterMetaType<mtx::responses::Notifications>();
-        qRegisterMetaType<mtx::responses::Rooms>();
-        qRegisterMetaType<mtx::responses::Sync>();
-        qRegisterMetaType<mtx::responses::JoinedGroups>();
-        qRegisterMetaType<mtx::responses::GroupProfile>();
-        qRegisterMetaType<std::string>();
-        qRegisterMetaType<nlohmann::json>();
-        qRegisterMetaType<std::vector<std::string>>();
-        qRegisterMetaType<std::vector<QString>>();
-        qRegisterMetaType<std::map<QString, bool>>("std::map<QString, bool>");
-        qRegisterMetaType<std::set<QString>>();
+    qRegisterMetaType<mtx::responses::Login>();
+    qRegisterMetaType<mtx::responses::Messages>();
+    qRegisterMetaType<mtx::responses::Notifications>();
+    qRegisterMetaType<mtx::responses::Rooms>();
+    qRegisterMetaType<mtx::responses::Sync>();
+    qRegisterMetaType<mtx::responses::JoinedGroups>();
+    qRegisterMetaType<mtx::responses::GroupProfile>();
+    qRegisterMetaType<std::string>();
+    qRegisterMetaType<nlohmann::json>();
+    qRegisterMetaType<std::vector<std::string>>();
+    qRegisterMetaType<std::vector<QString>>();
+    qRegisterMetaType<std::map<QString, bool>>("std::map<QString, bool>");
+    qRegisterMetaType<std::set<QString>>();
 }
 
 } // namespace http
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
index 0c0f0cddabc946ab888f55bc59b6065f1b7954e2..34730e9a1eabfdfd00561f7e6d5b0b41a9d62cad 100644
--- a/src/MemberList.cpp
+++ b/src/MemberList.cpp
@@ -15,98 +15,96 @@ MemberList::MemberList(const QString &room_id, QObject *parent)
   : QAbstractListModel{parent}
   , room_id_{room_id}
 {
-        try {
-                info_ = cache::singleRoomInfo(room_id_.toStdString());
-        } catch (const lmdb::error &) {
-                nhlog::db()->warn("failed to retrieve room info from cache: {}",
-                                  room_id_.toStdString());
-        }
+    try {
+        info_ = cache::singleRoomInfo(room_id_.toStdString());
+    } catch (const lmdb::error &) {
+        nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString());
+    }
 
-        try {
-                auto members = cache::getMembers(room_id_.toStdString());
-                addUsers(members);
-                numUsersLoaded_ = members.size();
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
-        }
+    try {
+        auto members = cache::getMembers(room_id_.toStdString());
+        addUsers(members);
+        numUsersLoaded_ = members.size();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
+    }
 }
 
 void
 MemberList::addUsers(const std::vector<RoomMember> &members)
 {
-        beginInsertRows(
-          QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1);
+    beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1);
 
-        for (const auto &member : members)
-                m_memberList.push_back(
-                  {member,
-                   ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl(
-                     member.user_id)});
+    for (const auto &member : members)
+        m_memberList.push_back(
+          {member,
+           ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl(
+             member.user_id)});
 
-        endInsertRows();
+    endInsertRows();
 }
 
 QHash<int, QByteArray>
 MemberList::roleNames() const
 {
-        return {
-          {Mxid, "mxid"},
-          {DisplayName, "displayName"},
-          {AvatarUrl, "avatarUrl"},
-          {Trustlevel, "trustlevel"},
-        };
+    return {
+      {Mxid, "mxid"},
+      {DisplayName, "displayName"},
+      {AvatarUrl, "avatarUrl"},
+      {Trustlevel, "trustlevel"},
+    };
 }
 
 QVariant
 MemberList::data(const QModelIndex &index, int role) const
 {
-        if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0)
-                return {};
+    if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0)
+        return {};
 
-        switch (role) {
-        case Mxid:
-                return m_memberList[index.row()].first.user_id;
-        case DisplayName:
-                return m_memberList[index.row()].first.display_name;
-        case AvatarUrl:
-                return m_memberList[index.row()].second;
-        case Trustlevel: {
-                auto stat =
-                  cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString());
+    switch (role) {
+    case Mxid:
+        return m_memberList[index.row()].first.user_id;
+    case DisplayName:
+        return m_memberList[index.row()].first.display_name;
+    case AvatarUrl:
+        return m_memberList[index.row()].second;
+    case Trustlevel: {
+        auto stat =
+          cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString());
 
-                if (!stat)
-                        return crypto::Unverified;
-                if (stat->unverified_device_count)
-                        return crypto::Unverified;
-                else
-                        return stat->user_verified;
-        }
-        default:
-                return {};
-        }
+        if (!stat)
+            return crypto::Unverified;
+        if (stat->unverified_device_count)
+            return crypto::Unverified;
+        else
+            return stat->user_verified;
+    }
+    default:
+        return {};
+    }
 }
 
 bool
 MemberList::canFetchMore(const QModelIndex &) const
 {
-        const size_t numMembers = rowCount();
-        if (numMembers > 1 && numMembers < info_.member_count)
-                return true;
-        else
-                return false;
+    const size_t numMembers = rowCount();
+    if (numMembers > 1 && numMembers < info_.member_count)
+        return true;
+    else
+        return false;
 }
 
 void
 MemberList::fetchMore(const QModelIndex &)
 {
-        loadingMoreMembers_ = true;
-        emit loadingMoreMembersChanged();
+    loadingMoreMembers_ = true;
+    emit loadingMoreMembersChanged();
 
-        auto members = cache::getMembers(room_id_.toStdString(), rowCount());
-        addUsers(members);
-        numUsersLoaded_ += members.size();
-        emit numUsersLoadedChanged();
+    auto members = cache::getMembers(room_id_.toStdString(), rowCount());
+    addUsers(members);
+    numUsersLoaded_ += members.size();
+    emit numUsersLoadedChanged();
 
-        loadingMoreMembers_ = false;
-        emit loadingMoreMembersChanged();
+    loadingMoreMembers_ = false;
+    emit loadingMoreMembersChanged();
 }
diff --git a/src/MemberList.h b/src/MemberList.h
index cffcd83d4b26713911081a9f4af4c1a1b2b71834..b16ac9838a92c1276ef6e3a8da57dfb29bcf4a4b 100644
--- a/src/MemberList.h
+++ b/src/MemberList.h
@@ -10,59 +10,59 @@
 
 class MemberList : public QAbstractListModel
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
-        Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged)
-        Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
-        Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged)
-        Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged)
-        Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged)
+    Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+    Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged)
+    Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
+    Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged)
+    Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged)
+    Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged)
 
 public:
-        enum Roles
-        {
-                Mxid,
-                DisplayName,
-                AvatarUrl,
-                Trustlevel,
-        };
-        MemberList(const QString &room_id, QObject *parent = nullptr);
+    enum Roles
+    {
+        Mxid,
+        DisplayName,
+        AvatarUrl,
+        Trustlevel,
+    };
+    MemberList(const QString &room_id, QObject *parent = nullptr);
 
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override
-        {
-                Q_UNUSED(parent)
-                return static_cast<int>(m_memberList.size());
-        }
-        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        Q_UNUSED(parent)
+        return static_cast<int>(m_memberList.size());
+    }
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
 
-        QString roomName() const { return QString::fromStdString(info_.name); }
-        int memberCount() const { return info_.member_count; }
-        QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); }
-        QString roomId() const { return room_id_; }
-        int numUsersLoaded() const { return numUsersLoaded_; }
-        bool loadingMoreMembers() const { return loadingMoreMembers_; }
+    QString roomName() const { return QString::fromStdString(info_.name); }
+    int memberCount() const { return info_.member_count; }
+    QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); }
+    QString roomId() const { return room_id_; }
+    int numUsersLoaded() const { return numUsersLoaded_; }
+    bool loadingMoreMembers() const { return loadingMoreMembers_; }
 
 signals:
-        void roomNameChanged();
-        void memberCountChanged();
-        void avatarUrlChanged();
-        void roomIdChanged();
-        void numUsersLoadedChanged();
-        void loadingMoreMembersChanged();
+    void roomNameChanged();
+    void memberCountChanged();
+    void avatarUrlChanged();
+    void roomIdChanged();
+    void numUsersLoadedChanged();
+    void loadingMoreMembersChanged();
 
 public slots:
-        void addUsers(const std::vector<RoomMember> &users);
+    void addUsers(const std::vector<RoomMember> &users);
 
 protected:
-        bool canFetchMore(const QModelIndex &) const override;
-        void fetchMore(const QModelIndex &) override;
+    bool canFetchMore(const QModelIndex &) const override;
+    void fetchMore(const QModelIndex &) override;
 
 private:
-        QVector<QPair<RoomMember, QString>> m_memberList;
-        QString room_id_;
-        RoomInfo info_;
-        int numUsersLoaded_{0};
-        bool loadingMoreMembers_{false};
+    QVector<QPair<RoomMember, QString>> m_memberList;
+    QString room_id_;
+    RoomInfo info_;
+    int numUsersLoaded_{0};
+    bool loadingMoreMembers_{false};
 };
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index 056374a992a5d88dbf82773cc251c20b3a8a0ad9..5d0ee0be5ba4bec876c7007585c35166aa7f9fc4 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -24,70 +24,70 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
 QQuickImageResponse *
 MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
 {
-        auto id_      = id;
-        bool crop     = true;
-        double radius = 0;
-
-        auto queryStart = id.lastIndexOf('?');
-        if (queryStart != -1) {
-                id_            = id.left(queryStart);
-                auto query     = id.midRef(queryStart + 1);
-                auto queryBits = query.split('&');
-
-                for (auto b : queryBits) {
-                        if (b == "scale") {
-                                crop = false;
-                        } else if (b.startsWith("radius=")) {
-                                radius = b.mid(7).toDouble();
-                        }
-                }
+    auto id_      = id;
+    bool crop     = true;
+    double radius = 0;
+
+    auto queryStart = id.lastIndexOf('?');
+    if (queryStart != -1) {
+        id_            = id.left(queryStart);
+        auto query     = id.midRef(queryStart + 1);
+        auto queryBits = query.split('&');
+
+        for (auto b : queryBits) {
+            if (b == "scale") {
+                crop = false;
+            } else if (b.startsWith("radius=")) {
+                radius = b.mid(7).toDouble();
+            }
         }
+    }
 
-        MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize);
-        pool.start(response);
-        return response;
+    MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize);
+    pool.start(response);
+    return response;
 }
 
 void
 MxcImageProvider::addEncryptionInfo(mtx::crypto::EncryptedFile info)
 {
-        infos.insert(QString::fromStdString(info.url), info);
+    infos.insert(QString::fromStdString(info.url), info);
 }
 void
 MxcImageResponse::run()
 {
-        MxcImageProvider::download(
-          m_id,
-          m_requestedSize,
-          [this](QString, QSize, QImage image, QString) {
-                  if (image.isNull()) {
-                          m_error = "Failed to download image.";
-                  } else {
-                          m_image = image;
-                  }
-                  emit finished();
-          },
-          m_crop,
-          m_radius);
+    MxcImageProvider::download(
+      m_id,
+      m_requestedSize,
+      [this](QString, QSize, QImage image, QString) {
+          if (image.isNull()) {
+              m_error = "Failed to download image.";
+          } else {
+              m_image = image;
+          }
+          emit finished();
+      },
+      m_crop,
+      m_radius);
 }
 
 static QImage
 clipRadius(QImage img, double radius)
 {
-        QImage out(img.size(), QImage::Format_ARGB32_Premultiplied);
-        out.fill(Qt::transparent);
+    QImage out(img.size(), QImage::Format_ARGB32_Premultiplied);
+    out.fill(Qt::transparent);
 
-        QPainter painter(&out);
-        painter.setRenderHint(QPainter::Antialiasing, true);
-        painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
+    QPainter painter(&out);
+    painter.setRenderHint(QPainter::Antialiasing, true);
+    painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
 
-        QPainterPath ppath;
-        ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
+    QPainterPath ppath;
+    ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
 
-        painter.setClipPath(ppath);
-        painter.drawImage(img.rect(), img);
+    painter.setClipPath(ppath);
+    painter.drawImage(img.rect(), img);
 
-        return out;
+    return out;
 }
 
 void
@@ -97,187 +97,165 @@ MxcImageProvider::download(const QString &id,
                            bool crop,
                            double radius)
 {
-        std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
-        auto temp = infos.find("mxc://" + id);
-        if (temp != infos.end())
-                encryptionInfo = *temp;
-
-        if (requestedSize.isValid() && !encryptionInfo) {
-                QString fileName =
-                  QString("%1_%2x%3_%4_radius%5")
-                    .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
-                                                                QByteArray::OmitTrailingEquals)))
-                    .arg(requestedSize.width())
-                    .arg(requestedSize.height())
-                    .arg(crop ? "crop" : "scale")
-                    .arg(radius);
-                QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
-                                     "/media_cache",
-                                   fileName);
-                QDir().mkpath(fileInfo.absolutePath());
-
-                if (fileInfo.exists()) {
-                        QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath());
-                        if (!image.isNull()) {
-                                image = image.scaled(
-                                  requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
-
-                                if (radius != 0) {
-                                        image = clipRadius(std::move(image), radius);
-                                }
-
-                                if (!image.isNull()) {
-                                        then(id, requestedSize, image, fileInfo.absoluteFilePath());
-                                        return;
-                                }
-                        }
+    std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
+    auto temp = infos.find("mxc://" + id);
+    if (temp != infos.end())
+        encryptionInfo = *temp;
+
+    if (requestedSize.isValid() && !encryptionInfo) {
+        QString fileName = QString("%1_%2x%3_%4_radius%5")
+                             .arg(QString::fromUtf8(id.toUtf8().toBase64(
+                               QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
+                             .arg(requestedSize.width())
+                             .arg(requestedSize.height())
+                             .arg(crop ? "crop" : "scale")
+                             .arg(radius);
+        QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
+                             "/media_cache",
+                           fileName);
+        QDir().mkpath(fileInfo.absolutePath());
+
+        if (fileInfo.exists()) {
+            QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath());
+            if (!image.isNull()) {
+                image = image.scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+
+                if (radius != 0) {
+                    image = clipRadius(std::move(image), radius);
                 }
 
-                mtx::http::ThumbOpts opts;
-                opts.mxc_url = "mxc://" + id.toStdString();
-                opts.width   = requestedSize.width() > 0 ? requestedSize.width() : -1;
-                opts.height  = requestedSize.height() > 0 ? requestedSize.height() : -1;
-                opts.method  = crop ? "crop" : "scale";
-                http::client()->get_thumbnail(
-                  opts,
-                  [fileInfo, requestedSize, radius, then, id](const std::string &res,
-                                                              mtx::http::RequestErr err) {
-                          if (err || res.empty()) {
-                                  then(id, QSize(), {}, "");
-
-                                  return;
-                          }
-
-                          auto data    = QByteArray(res.data(), (int)res.size());
-                          QImage image = utils::readImage(data);
-                          if (!image.isNull()) {
-                                  image = image.scaled(
-                                    requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
-
-                                  if (radius != 0) {
-                                          image = clipRadius(std::move(image), radius);
-                                  }
-                          }
-                          image.setText("mxc url", "mxc://" + id);
-                          if (image.save(fileInfo.absoluteFilePath(), "png"))
-                                  nhlog::ui()->debug("Wrote: {}",
-                                                     fileInfo.absoluteFilePath().toStdString());
-                          else
-                                  nhlog::ui()->debug("Failed to write: {}",
-                                                     fileInfo.absoluteFilePath().toStdString());
-
-                          then(id, requestedSize, image, fileInfo.absoluteFilePath());
-                  });
-        } else {
-                try {
-                        QString fileName =
-                          QString("%1_radius%2")
-                            .arg(QString::fromUtf8(id.toUtf8().toBase64(
-                              QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
-                            .arg(radius);
-
-                        QFileInfo fileInfo(
-                          QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
-                            "/media_cache",
-                          fileName);
-                        QDir().mkpath(fileInfo.absolutePath());
-
-                        if (fileInfo.exists()) {
-                                if (encryptionInfo) {
-                                        QFile f(fileInfo.absoluteFilePath());
-                                        f.open(QIODevice::ReadOnly);
-
-                                        QByteArray fileData = f.readAll();
-                                        auto tempData =
-                                          mtx::crypto::to_string(mtx::crypto::decrypt_file(
-                                            fileData.toStdString(), encryptionInfo.value()));
-                                        auto data =
-                                          QByteArray(tempData.data(), (int)tempData.size());
-                                        QImage image = utils::readImage(data);
-                                        image.setText("mxc url", "mxc://" + id);
-                                        if (!image.isNull()) {
-                                                if (radius != 0) {
-                                                        image =
-                                                          clipRadius(std::move(image), radius);
-                                                }
-
-                                                then(id,
-                                                     requestedSize,
-                                                     image,
-                                                     fileInfo.absoluteFilePath());
-                                                return;
-                                        }
-                                } else {
-                                        QImage image =
-                                          utils::readImageFromFile(fileInfo.absoluteFilePath());
-                                        if (!image.isNull()) {
-                                                if (radius != 0) {
-                                                        image =
-                                                          clipRadius(std::move(image), radius);
-                                                }
-
-                                                then(id,
-                                                     requestedSize,
-                                                     image,
-                                                     fileInfo.absoluteFilePath());
-                                                return;
-                                        }
-                                }
+                if (!image.isNull()) {
+                    then(id, requestedSize, image, fileInfo.absoluteFilePath());
+                    return;
+                }
+            }
+        }
+
+        mtx::http::ThumbOpts opts;
+        opts.mxc_url = "mxc://" + id.toStdString();
+        opts.width   = requestedSize.width() > 0 ? requestedSize.width() : -1;
+        opts.height  = requestedSize.height() > 0 ? requestedSize.height() : -1;
+        opts.method  = crop ? "crop" : "scale";
+        http::client()->get_thumbnail(
+          opts,
+          [fileInfo, requestedSize, radius, then, id](const std::string &res,
+                                                      mtx::http::RequestErr err) {
+              if (err || res.empty()) {
+                  then(id, QSize(), {}, "");
+
+                  return;
+              }
+
+              auto data    = QByteArray(res.data(), (int)res.size());
+              QImage image = utils::readImage(data);
+              if (!image.isNull()) {
+                  image =
+                    image.scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+
+                  if (radius != 0) {
+                      image = clipRadius(std::move(image), radius);
+                  }
+              }
+              image.setText("mxc url", "mxc://" + id);
+              if (image.save(fileInfo.absoluteFilePath(), "png"))
+                  nhlog::ui()->debug("Wrote: {}", fileInfo.absoluteFilePath().toStdString());
+              else
+                  nhlog::ui()->debug("Failed to write: {}",
+                                     fileInfo.absoluteFilePath().toStdString());
+
+              then(id, requestedSize, image, fileInfo.absoluteFilePath());
+          });
+    } else {
+        try {
+            QString fileName = QString("%1_radius%2")
+                                 .arg(QString::fromUtf8(id.toUtf8().toBase64(
+                                   QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
+                                 .arg(radius);
+
+            QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
+                                 "/media_cache",
+                               fileName);
+            QDir().mkpath(fileInfo.absolutePath());
+
+            if (fileInfo.exists()) {
+                if (encryptionInfo) {
+                    QFile f(fileInfo.absoluteFilePath());
+                    f.open(QIODevice::ReadOnly);
+
+                    QByteArray fileData = f.readAll();
+                    auto tempData       = mtx::crypto::to_string(
+                      mtx::crypto::decrypt_file(fileData.toStdString(), encryptionInfo.value()));
+                    auto data    = QByteArray(tempData.data(), (int)tempData.size());
+                    QImage image = utils::readImage(data);
+                    image.setText("mxc url", "mxc://" + id);
+                    if (!image.isNull()) {
+                        if (radius != 0) {
+                            image = clipRadius(std::move(image), radius);
                         }
 
-                        http::client()->download(
-                          "mxc://" + id.toStdString(),
-                          [fileInfo, requestedSize, then, id, radius, encryptionInfo](
-                            const std::string &res,
-                            const std::string &,
-                            const std::string &originalFilename,
-                            mtx::http::RequestErr err) {
-                                  if (err) {
-                                          then(id, QSize(), {}, "");
-                                          return;
-                                  }
-
-                                  auto tempData = res;
-                                  QFile f(fileInfo.absoluteFilePath());
-                                  if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
-                                          then(id, QSize(), {}, "");
-                                          return;
-                                  }
-                                  f.write(tempData.data(), tempData.size());
-                                  f.close();
-
-                                  if (encryptionInfo) {
-                                          tempData =
-                                            mtx::crypto::to_string(mtx::crypto::decrypt_file(
-                                              tempData, encryptionInfo.value()));
-                                          auto data =
-                                            QByteArray(tempData.data(), (int)tempData.size());
-                                          QImage image = utils::readImage(data);
-                                          if (radius != 0) {
-                                                  image = clipRadius(std::move(image), radius);
-                                          }
-
-                                          image.setText("original filename",
-                                                        QString::fromStdString(originalFilename));
-                                          image.setText("mxc url", "mxc://" + id);
-                                          then(
-                                            id, requestedSize, image, fileInfo.absoluteFilePath());
-                                          return;
-                                  }
-
-                                  QImage image =
-                                    utils::readImageFromFile(fileInfo.absoluteFilePath());
-                                  if (radius != 0) {
-                                          image = clipRadius(std::move(image), radius);
-                                  }
-
-                                  image.setText("original filename",
-                                                QString::fromStdString(originalFilename));
-                                  image.setText("mxc url", "mxc://" + id);
-                                  then(id, requestedSize, image, fileInfo.absoluteFilePath());
-                          });
-                } catch (std::exception &e) {
-                        nhlog::net()->error("Exception while downloading media: {}", e.what());
+                        then(id, requestedSize, image, fileInfo.absoluteFilePath());
+                        return;
+                    }
+                } else {
+                    QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath());
+                    if (!image.isNull()) {
+                        if (radius != 0) {
+                            image = clipRadius(std::move(image), radius);
+                        }
+
+                        then(id, requestedSize, image, fileInfo.absoluteFilePath());
+                        return;
+                    }
                 }
+            }
+
+            http::client()->download(
+              "mxc://" + id.toStdString(),
+              [fileInfo, requestedSize, then, id, radius, encryptionInfo](
+                const std::string &res,
+                const std::string &,
+                const std::string &originalFilename,
+                mtx::http::RequestErr err) {
+                  if (err) {
+                      then(id, QSize(), {}, "");
+                      return;
+                  }
+
+                  auto tempData = res;
+                  QFile f(fileInfo.absoluteFilePath());
+                  if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
+                      then(id, QSize(), {}, "");
+                      return;
+                  }
+                  f.write(tempData.data(), tempData.size());
+                  f.close();
+
+                  if (encryptionInfo) {
+                      tempData = mtx::crypto::to_string(
+                        mtx::crypto::decrypt_file(tempData, encryptionInfo.value()));
+                      auto data    = QByteArray(tempData.data(), (int)tempData.size());
+                      QImage image = utils::readImage(data);
+                      if (radius != 0) {
+                          image = clipRadius(std::move(image), radius);
+                      }
+
+                      image.setText("original filename", QString::fromStdString(originalFilename));
+                      image.setText("mxc url", "mxc://" + id);
+                      then(id, requestedSize, image, fileInfo.absoluteFilePath());
+                      return;
+                  }
+
+                  QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath());
+                  if (radius != 0) {
+                      image = clipRadius(std::move(image), radius);
+                  }
+
+                  image.setText("original filename", QString::fromStdString(originalFilename));
+                  image.setText("mxc url", "mxc://" + id);
+                  then(id, requestedSize, image, fileInfo.absoluteFilePath());
+              });
+        } catch (std::exception &e) {
+            nhlog::net()->error("Exception while downloading media: {}", e.what());
         }
+    }
 }
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
index 6de83c0e3d17a9e539778daf36e987c093299bf3..3cf5bbf456c6683d30f247a20dfc8045805cf867 100644
--- a/src/MxcImageProvider.h
+++ b/src/MxcImageProvider.h
@@ -19,46 +19,46 @@ class MxcImageResponse
   , public QRunnable
 {
 public:
-        MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize)
-          : m_id(id)
-          , m_requestedSize(requestedSize)
-          , m_crop(crop)
-          , m_radius(radius)
-        {
-                setAutoDelete(false);
-        }
+    MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize)
+      : m_id(id)
+      , m_requestedSize(requestedSize)
+      , m_crop(crop)
+      , m_radius(radius)
+    {
+        setAutoDelete(false);
+    }
 
-        QQuickTextureFactory *textureFactory() const override
-        {
-                return QQuickTextureFactory::textureFactoryForImage(m_image);
-        }
-        QString errorString() const override { return m_error; }
+    QQuickTextureFactory *textureFactory() const override
+    {
+        return QQuickTextureFactory::textureFactoryForImage(m_image);
+    }
+    QString errorString() const override { return m_error; }
 
-        void run() override;
+    void run() override;
 
-        QString m_id, m_error;
-        QSize m_requestedSize;
-        QImage m_image;
-        bool m_crop;
-        double m_radius;
+    QString m_id, m_error;
+    QSize m_requestedSize;
+    QImage m_image;
+    bool m_crop;
+    double m_radius;
 };
 
 class MxcImageProvider
   : public QObject
   , public QQuickAsyncImageProvider
 {
-        Q_OBJECT
+    Q_OBJECT
 public slots:
-        QQuickImageResponse *requestImageResponse(const QString &id,
-                                                  const QSize &requestedSize) override;
+    QQuickImageResponse *requestImageResponse(const QString &id,
+                                              const QSize &requestedSize) override;
 
-        static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
-        static void download(const QString &id,
-                             const QSize &requestedSize,
-                             std::function<void(QString, QSize, QImage, QString)> then,
-                             bool crop     = true,
-                             double radius = 0);
+    static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
+    static void download(const QString &id,
+                         const QSize &requestedSize,
+                         std::function<void(QString, QSize, QImage, QString)> then,
+                         bool crop     = true,
+                         double radius = 0);
 
 private:
-        QThreadPool pool;
+    QThreadPool pool;
 };
diff --git a/src/Olm.cpp b/src/Olm.cpp
deleted file mode 100644
index 2c9ac5a33daaa81fee1b94802d933c010e1b424a..0000000000000000000000000000000000000000
--- a/src/Olm.cpp
+++ /dev/null
@@ -1,1576 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "Olm.h"
-
-#include <QObject>
-#include <QTimer>
-
-#include <nlohmann/json.hpp>
-#include <variant>
-
-#include <mtx/responses/common.hpp>
-#include <mtx/secret_storage.hpp>
-
-#include "Cache.h"
-#include "Cache_p.h"
-#include "ChatPage.h"
-#include "DeviceVerificationFlow.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-
-namespace {
-auto client_ = std::make_unique<mtx::crypto::OlmClient>();
-
-std::map<std::string, std::string> request_id_to_secret_name;
-
-const std::string STORAGE_SECRET_KEY("secret");
-constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
-}
-
-namespace olm {
-void
-from_json(const nlohmann::json &obj, OlmMessage &msg)
-{
-        if (obj.at("type") != "m.room.encrypted")
-                throw std::invalid_argument("invalid type for olm message");
-
-        if (obj.at("content").at("algorithm") != OLM_ALGO)
-                throw std::invalid_argument("invalid algorithm for olm message");
-
-        msg.sender     = obj.at("sender");
-        msg.sender_key = obj.at("content").at("sender_key");
-        msg.ciphertext = obj.at("content")
-                           .at("ciphertext")
-                           .get<std::map<std::string, mtx::events::msg::OlmCipherContent>>();
-}
-
-mtx::crypto::OlmClient *
-client()
-{
-        return client_.get();
-}
-
-static void
-handle_secret_request(const mtx::events::DeviceEvent<mtx::events::msg::SecretRequest> *e,
-                      const std::string &sender)
-{
-        using namespace mtx::events;
-
-        if (e->content.action != mtx::events::msg::RequestAction::Request)
-                return;
-
-        auto local_user = http::client()->user_id();
-
-        if (sender != local_user.to_string())
-                return;
-
-        auto verificationStatus = cache::verificationStatus(local_user.to_string());
-
-        if (!verificationStatus)
-                return;
-
-        auto deviceKeys = cache::userKeys(local_user.to_string());
-        if (!deviceKeys)
-                return;
-
-        if (std::find(verificationStatus->verified_devices.begin(),
-                      verificationStatus->verified_devices.end(),
-                      e->content.requesting_device_id) ==
-            verificationStatus->verified_devices.end())
-                return;
-
-        // this is a verified device
-        mtx::events::DeviceEvent<mtx::events::msg::SecretSend> secretSend;
-        secretSend.type               = EventType::SecretSend;
-        secretSend.content.request_id = e->content.request_id;
-
-        auto secret = cache::client()->secret(e->content.name);
-        if (!secret)
-                return;
-        secretSend.content.secret = secret.value();
-
-        send_encrypted_to_device_messages(
-          {{local_user.to_string(), {{e->content.requesting_device_id}}}}, secretSend);
-
-        nhlog::net()->info("Sent secret '{}' to ({},{})",
-                           e->content.name,
-                           local_user.to_string(),
-                           e->content.requesting_device_id);
-}
-
-void
-handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEvents> &msgs)
-{
-        if (msgs.empty())
-                return;
-        nhlog::crypto()->info("received {} to_device messages", msgs.size());
-        nlohmann::json j_msg;
-
-        for (const auto &msg : msgs) {
-                j_msg = std::visit([](auto &e) { return json(e); }, std::move(msg));
-                if (j_msg.count("type") == 0) {
-                        nhlog::crypto()->warn("received message with no type field: {}",
-                                              j_msg.dump(2));
-                        continue;
-                }
-
-                std::string msg_type = j_msg.at("type");
-
-                if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) {
-                        try {
-                                olm::OlmMessage olm_msg = j_msg;
-                                cache::client()->query_keys(
-                                  olm_msg.sender,
-                                  [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) {
-                                          if (e) {
-                                                  nhlog::crypto()->error(
-                                                    "Failed to query user keys, dropping olm "
-                                                    "message");
-                                                  return;
-                                          }
-                                          handle_olm_message(std::move(olm_msg), userKeys);
-                                  });
-                        } catch (const nlohmann::json::exception &e) {
-                                nhlog::crypto()->warn(
-                                  "parsing error for olm message: {} {}", e.what(), j_msg.dump(2));
-                        } catch (const std::invalid_argument &e) {
-                                nhlog::crypto()->warn("validation error for olm message: {} {}",
-                                                      e.what(),
-                                                      j_msg.dump(2));
-                        }
-
-                } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) {
-                        nhlog::crypto()->warn("handling key request event: {}", j_msg.dump(2));
-                        try {
-                                mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> req = j_msg;
-                                if (req.content.action == mtx::events::msg::RequestAction::Request)
-                                        handle_key_request_message(req);
-                                else
-                                        nhlog::crypto()->warn(
-                                          "ignore key request (unhandled action): {}",
-                                          req.content.request_id);
-                        } catch (const nlohmann::json::exception &e) {
-                                nhlog::crypto()->warn(
-                                  "parsing error for key_request message: {} {}",
-                                  e.what(),
-                                  j_msg.dump(2));
-                        }
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
-                        auto message = std::get<
-                          mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationAccept>>(msg);
-                        ChatPage::instance()->receivedDeviceVerificationAccept(message.content);
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) {
-                        auto message = std::get<
-                          mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationRequest>>(msg);
-                        ChatPage::instance()->receivedDeviceVerificationRequest(message.content,
-                                                                                message.sender);
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) {
-                        auto message = std::get<
-                          mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationCancel>>(msg);
-                        ChatPage::instance()->receivedDeviceVerificationCancel(message.content);
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) {
-                        auto message =
-                          std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationKey>>(
-                            msg);
-                        ChatPage::instance()->receivedDeviceVerificationKey(message.content);
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) {
-                        auto message =
-                          std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationMac>>(
-                            msg);
-                        ChatPage::instance()->receivedDeviceVerificationMac(message.content);
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) {
-                        auto message = std::get<
-                          mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationStart>>(msg);
-                        ChatPage::instance()->receivedDeviceVerificationStart(message.content,
-                                                                              message.sender);
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) {
-                        auto message = std::get<
-                          mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationReady>>(msg);
-                        ChatPage::instance()->receivedDeviceVerificationReady(message.content);
-                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) {
-                        auto message =
-                          std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>(
-                            msg);
-                        ChatPage::instance()->receivedDeviceVerificationDone(message.content);
-                } else if (auto e =
-                             std::get_if<mtx::events::DeviceEvent<mtx::events::msg::SecretRequest>>(
-                               &msg)) {
-                        handle_secret_request(e, e->sender);
-                } else {
-                        nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
-                }
-        }
-}
-
-void
-handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys)
-{
-        nhlog::crypto()->info("sender    : {}", msg.sender);
-        nhlog::crypto()->info("sender_key: {}", msg.sender_key);
-
-        if (msg.sender_key == olm::client()->identity_keys().ed25519) {
-                nhlog::crypto()->warn("Ignoring olm message from ourselves!");
-                return;
-        }
-
-        const auto my_key = olm::client()->identity_keys().curve25519;
-
-        bool failed_decryption = false;
-
-        for (const auto &cipher : msg.ciphertext) {
-                // We skip messages not meant for the current device.
-                if (cipher.first != my_key) {
-                        nhlog::crypto()->debug(
-                          "Skipping message for {} since we are {}.", cipher.first, my_key);
-                        continue;
-                }
-
-                const auto type = cipher.second.type;
-                nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
-
-                auto payload = try_olm_decryption(msg.sender_key, cipher.second);
-
-                if (payload.is_null()) {
-                        // Check for PRE_KEY message
-                        if (cipher.second.type == 0) {
-                                payload = handle_pre_key_olm_message(
-                                  msg.sender, msg.sender_key, cipher.second);
-                        } else {
-                                nhlog::crypto()->error("Undecryptable olm message!");
-                                failed_decryption = true;
-                                continue;
-                        }
-                }
-
-                if (!payload.is_null()) {
-                        mtx::events::collections::DeviceEvents device_event;
-
-                        // Other properties are included in order to prevent an attacker from
-                        // publishing someone else's curve25519 keys as their own and subsequently
-                        // claiming to have sent messages which they didn't. sender must correspond
-                        // to the user who sent the event, recipient to the local user, and
-                        // recipient_keys to the local ed25519 key.
-                        std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"];
-                        if (receiver_ed25519.empty() ||
-                            receiver_ed25519 != olm::client()->identity_keys().ed25519) {
-                                nhlog::crypto()->warn(
-                                  "Decrypted event doesn't include our ed25519: {}",
-                                  payload.dump());
-                                return;
-                        }
-                        std::string receiver = payload["recipient"];
-                        if (receiver.empty() || receiver != http::client()->user_id().to_string()) {
-                                nhlog::crypto()->warn(
-                                  "Decrypted event doesn't include our user_id: {}",
-                                  payload.dump());
-                                return;
-                        }
-
-                        // Clients must confirm that the sender_key and the ed25519 field value
-                        // under the keys property match the keys returned by /keys/query for the
-                        // given user, and must also verify the signature of the payload. Without
-                        // this check, a client cannot be sure that the sender device owns the
-                        // private part of the ed25519 key it claims to have in the Olm payload.
-                        // This is crucial when the ed25519 key corresponds to a verified device.
-                        std::string sender_ed25519 = payload["keys"]["ed25519"];
-                        if (sender_ed25519.empty()) {
-                                nhlog::crypto()->warn(
-                                  "Decrypted event doesn't include sender ed25519: {}",
-                                  payload.dump());
-                                return;
-                        }
-
-                        bool from_their_device = false;
-                        for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
-                                auto c_key = key.keys.find("curve25519:" + device_id);
-                                auto e_key = key.keys.find("ed25519:" + device_id);
-
-                                if (c_key == key.keys.end() || e_key == key.keys.end()) {
-                                        nhlog::crypto()->warn(
-                                          "Skipping device {} as we have no keys for it.",
-                                          device_id);
-                                } else if (c_key->second == msg.sender_key &&
-                                           e_key->second == sender_ed25519) {
-                                        from_their_device = true;
-                                        break;
-                                }
-                        }
-                        if (!from_their_device) {
-                                nhlog::crypto()->warn("Decrypted event isn't sent from a device "
-                                                      "listed by that user! {}",
-                                                      payload.dump());
-                                return;
-                        }
-
-                        {
-                                std::string msg_type = payload["type"];
-                                json event_array     = json::array();
-                                event_array.push_back(payload);
-
-                                std::vector<mtx::events::collections::DeviceEvents> temp_events;
-                                mtx::responses::utils::parse_device_events(event_array,
-                                                                           temp_events);
-                                if (temp_events.empty()) {
-                                        nhlog::crypto()->warn("Decrypted unknown event: {}",
-                                                              payload.dump());
-                                        return;
-                                }
-                                device_event = temp_events.at(0);
-                        }
-
-                        using namespace mtx::events;
-                        if (auto e1 =
-                              std::get_if<DeviceEvent<msg::KeyVerificationAccept>>(&device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationAccept(e1->content);
-                        } else if (auto e2 = std::get_if<DeviceEvent<msg::KeyVerificationRequest>>(
-                                     &device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationRequest(e2->content,
-                                                                                        e2->sender);
-                        } else if (auto e3 = std::get_if<DeviceEvent<msg::KeyVerificationCancel>>(
-                                     &device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationCancel(e3->content);
-                        } else if (auto e4 = std::get_if<DeviceEvent<msg::KeyVerificationKey>>(
-                                     &device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationKey(e4->content);
-                        } else if (auto e5 = std::get_if<DeviceEvent<msg::KeyVerificationMac>>(
-                                     &device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationMac(e5->content);
-                        } else if (auto e6 = std::get_if<DeviceEvent<msg::KeyVerificationStart>>(
-                                     &device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationStart(e6->content,
-                                                                                      e6->sender);
-                        } else if (auto e7 = std::get_if<DeviceEvent<msg::KeyVerificationReady>>(
-                                     &device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationReady(e7->content);
-                        } else if (auto e8 = std::get_if<DeviceEvent<msg::KeyVerificationDone>>(
-                                     &device_event)) {
-                                ChatPage::instance()->receivedDeviceVerificationDone(e8->content);
-                        } else if (auto roomKey =
-                                     std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) {
-                                create_inbound_megolm_session(
-                                  *roomKey, msg.sender_key, sender_ed25519);
-                        } else if (auto forwardedRoomKey =
-                                     std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(
-                                       &device_event)) {
-                                forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back(
-                                  msg.sender_key);
-                                import_inbound_megolm_session(*forwardedRoomKey);
-                        } else if (auto e =
-                                     std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) {
-                                auto local_user = http::client()->user_id();
-
-                                if (msg.sender != local_user.to_string())
-                                        return;
-
-                                auto secret_name =
-                                  request_id_to_secret_name.find(e->content.request_id);
-
-                                if (secret_name != request_id_to_secret_name.end()) {
-                                        nhlog::crypto()->info("Received secret: {}",
-                                                              secret_name->second);
-
-                                        mtx::events::msg::SecretRequest secretRequest{};
-                                        secretRequest.action =
-                                          mtx::events::msg::RequestAction::Cancellation;
-                                        secretRequest.requesting_device_id =
-                                          http::client()->device_id();
-                                        secretRequest.request_id = e->content.request_id;
-
-                                        auto verificationStatus =
-                                          cache::verificationStatus(local_user.to_string());
-
-                                        if (!verificationStatus)
-                                                return;
-
-                                        auto deviceKeys = cache::userKeys(local_user.to_string());
-                                        std::string sender_device_id;
-                                        if (deviceKeys) {
-                                                for (auto &[dev, key] : deviceKeys->device_keys) {
-                                                        if (key.keys["curve25519:" + dev] ==
-                                                            msg.sender_key) {
-                                                                sender_device_id = dev;
-                                                                break;
-                                                        }
-                                                }
-                                        }
-
-                                        std::map<
-                                          mtx::identifiers::User,
-                                          std::map<std::string, mtx::events::msg::SecretRequest>>
-                                          body;
-
-                                        for (const auto &dev :
-                                             verificationStatus->verified_devices) {
-                                                if (dev != secretRequest.requesting_device_id &&
-                                                    dev != sender_device_id)
-                                                        body[local_user][dev] = secretRequest;
-                                        }
-
-                                        http::client()
-                                          ->send_to_device<mtx::events::msg::SecretRequest>(
-                                            http::client()->generate_txn_id(),
-                                            body,
-                                            [name =
-                                               secret_name->second](mtx::http::RequestErr err) {
-                                                    if (err) {
-                                                            nhlog::net()->error(
-                                                              "Failed to send request cancellation "
-                                                              "for secrect "
-                                                              "'{}'",
-                                                              name);
-                                                    }
-                                            });
-
-                                        nhlog::crypto()->info("Storing secret {}",
-                                                              secret_name->second);
-                                        cache::client()->storeSecret(secret_name->second,
-                                                                     e->content.secret);
-
-                                        request_id_to_secret_name.erase(secret_name);
-                                }
-
-                        } else if (auto sec_req =
-                                     std::get_if<DeviceEvent<msg::SecretRequest>>(&device_event)) {
-                                handle_secret_request(sec_req, msg.sender);
-                        }
-
-                        return;
-                } else {
-                        failed_decryption = true;
-                }
-        }
-
-        if (failed_decryption) {
-                try {
-                        std::map<std::string, std::vector<std::string>> targets;
-                        for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
-                                if (key.keys.at("curve25519:" + device_id) == msg.sender_key)
-                                        targets[msg.sender].push_back(device_id);
-                        }
-
-                        send_encrypted_to_device_messages(
-                          targets, mtx::events::DeviceEvent<mtx::events::msg::Dummy>{}, true);
-                        nhlog::crypto()->info("Recovering from broken olm channel with {}:{}",
-                                              msg.sender,
-                                              msg.sender_key);
-                } catch (std::exception &e) {
-                        nhlog::crypto()->error("Failed to recover from broken olm sessions: {}",
-                                               e.what());
-                }
-        }
-}
-
-nlohmann::json
-handle_pre_key_olm_message(const std::string &sender,
-                           const std::string &sender_key,
-                           const mtx::events::msg::OlmCipherContent &content)
-{
-        nhlog::crypto()->info("opening olm session with {}", sender);
-
-        mtx::crypto::OlmSessionPtr inbound_session = nullptr;
-        try {
-                inbound_session =
-                  olm::client()->create_inbound_session_from(sender_key, content.body);
-
-                // We also remove the one time key used to establish that
-                // session so we'll have to update our copy of the account object.
-                cache::saveOlmAccount(olm::client()->save("secret"));
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical(
-                  "failed to create inbound session with {}: {}", sender, e.what());
-                return {};
-        }
-
-        if (!mtx::crypto::matches_inbound_session_from(
-              inbound_session.get(), sender_key, content.body)) {
-                nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})",
-                                      sender);
-                return {};
-        }
-
-        mtx::crypto::BinaryBuf output;
-        try {
-                output =
-                  olm::client()->decrypt_message(inbound_session.get(), content.type, content.body);
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical(
-                  "failed to decrypt olm message {}: {}", content.body, e.what());
-                return {};
-        }
-
-        auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
-        nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2));
-
-        try {
-                nhlog::crypto()->debug("New olm session: {}",
-                                       mtx::crypto::session_id(inbound_session.get()));
-                cache::saveOlmSession(
-                  sender_key, std::move(inbound_session), QDateTime::currentMSecsSinceEpoch());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn(
-                  "failed to save inbound olm session from {}: {}", sender, e.what());
-        }
-
-        return plaintext;
-}
-
-mtx::events::msg::Encrypted
-encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body)
-{
-        using namespace mtx::events;
-        using namespace mtx::identifiers;
-
-        auto own_user_id = http::client()->user_id().to_string();
-
-        auto members = cache::client()->getMembersWithKeys(
-          room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers());
-
-        std::map<std::string, std::vector<std::string>> sendSessionTo;
-        mtx::crypto::OutboundGroupSessionPtr session = nullptr;
-        GroupSessionData group_session_data;
-
-        if (cache::outboundMegolmSessionExists(room_id)) {
-                auto res                = cache::getOutboundMegolmSession(room_id);
-                auto encryptionSettings = cache::client()->roomEncryptionSettings(room_id);
-                mtx::events::state::Encryption defaultSettings;
-
-                // rotate if we crossed the limits for this key
-                if (res.data.message_index <
-                      encryptionSettings.value_or(defaultSettings).rotation_period_msgs &&
-                    (QDateTime::currentMSecsSinceEpoch() - res.data.timestamp) <
-                      encryptionSettings.value_or(defaultSettings).rotation_period_ms) {
-                        auto member_it             = members.begin();
-                        auto session_member_it     = res.data.currently.keys.begin();
-                        auto session_member_it_end = res.data.currently.keys.end();
-
-                        while (member_it != members.end() ||
-                               session_member_it != session_member_it_end) {
-                                if (member_it == members.end()) {
-                                        // a member left, purge session!
-                                        nhlog::crypto()->debug(
-                                          "Rotating megolm session because of left member");
-                                        break;
-                                }
-
-                                if (session_member_it == session_member_it_end) {
-                                        // share with all remaining members
-                                        while (member_it != members.end()) {
-                                                sendSessionTo[member_it->first] = {};
-
-                                                if (member_it->second)
-                                                        for (const auto &dev :
-                                                             member_it->second->device_keys)
-                                                                if (member_it->first !=
-                                                                      own_user_id ||
-                                                                    dev.first != device_id)
-                                                                        sendSessionTo[member_it
-                                                                                        ->first]
-                                                                          .push_back(dev.first);
-
-                                                ++member_it;
-                                        }
-
-                                        session = std::move(res.session);
-                                        break;
-                                }
-
-                                if (member_it->first > session_member_it->first) {
-                                        // a member left, purge session
-                                        nhlog::crypto()->debug(
-                                          "Rotating megolm session because of left member");
-                                        break;
-                                } else if (member_it->first < session_member_it->first) {
-                                        // new member, send them the session at this index
-                                        sendSessionTo[member_it->first] = {};
-
-                                        if (member_it->second) {
-                                                for (const auto &dev :
-                                                     member_it->second->device_keys)
-                                                        if (member_it->first != own_user_id ||
-                                                            dev.first != device_id)
-                                                                sendSessionTo[member_it->first]
-                                                                  .push_back(dev.first);
-                                        }
-
-                                        ++member_it;
-                                } else {
-                                        // compare devices
-                                        bool device_removed = false;
-                                        for (const auto &dev :
-                                             session_member_it->second.deviceids) {
-                                                if (!member_it->second ||
-                                                    !member_it->second->device_keys.count(
-                                                      dev.first)) {
-                                                        device_removed = true;
-                                                        break;
-                                                }
-                                        }
-
-                                        if (device_removed) {
-                                                // device removed, rotate session!
-                                                nhlog::crypto()->debug(
-                                                  "Rotating megolm session because of removed "
-                                                  "device of {}",
-                                                  member_it->first);
-                                                break;
-                                        }
-
-                                        // check for new devices to share with
-                                        if (member_it->second)
-                                                for (const auto &dev :
-                                                     member_it->second->device_keys)
-                                                        if (!session_member_it->second.deviceids
-                                                               .count(dev.first) &&
-                                                            (member_it->first != own_user_id ||
-                                                             dev.first != device_id))
-                                                                sendSessionTo[member_it->first]
-                                                                  .push_back(dev.first);
-
-                                        ++member_it;
-                                        ++session_member_it;
-                                        if (member_it == members.end() &&
-                                            session_member_it == session_member_it_end) {
-                                                // all devices match or are newly added
-                                                session = std::move(res.session);
-                                        }
-                                }
-                        }
-                }
-
-                group_session_data = std::move(res.data);
-        }
-
-        if (!session) {
-                nhlog::ui()->debug("creating new outbound megolm session");
-
-                // Create a new outbound megolm session.
-                session                = olm::client()->init_outbound_group_session();
-                const auto session_id  = mtx::crypto::session_id(session.get());
-                const auto session_key = mtx::crypto::session_key(session.get());
-
-                // Saving the new megolm session.
-                GroupSessionData session_data{};
-                session_data.message_index              = 0;
-                session_data.timestamp                  = QDateTime::currentMSecsSinceEpoch();
-                session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519;
-
-                sendSessionTo.clear();
-
-                for (const auto &[user, devices] : members) {
-                        sendSessionTo[user]               = {};
-                        session_data.currently.keys[user] = {};
-                        if (devices) {
-                                for (const auto &[device_id_, key] : devices->device_keys) {
-                                        (void)key;
-                                        if (device_id != device_id_ || user != own_user_id) {
-                                                sendSessionTo[user].push_back(device_id_);
-                                                session_data.currently.keys[user]
-                                                  .deviceids[device_id_] = 0;
-                                        }
-                                }
-                        }
-                }
-
-                {
-                        MegolmSessionIndex index;
-                        index.room_id    = room_id;
-                        index.session_id = session_id;
-                        index.sender_key = olm::client()->identity_keys().curve25519;
-                        auto megolm_session =
-                          olm::client()->init_inbound_group_session(session_key);
-                        cache::saveInboundMegolmSession(
-                          index, std::move(megolm_session), session_data);
-                }
-
-                cache::saveOutboundMegolmSession(room_id, session_data, session);
-                group_session_data = std::move(session_data);
-        }
-
-        mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload{};
-        megolm_payload.content.algorithm   = MEGOLM_ALGO;
-        megolm_payload.content.room_id     = room_id;
-        megolm_payload.content.session_id  = mtx::crypto::session_id(session.get());
-        megolm_payload.content.session_key = mtx::crypto::session_key(session.get());
-        megolm_payload.type                = mtx::events::EventType::RoomKey;
-
-        if (!sendSessionTo.empty())
-                olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload);
-
-        // relations shouldn't be encrypted...
-        mtx::common::Relations relations = mtx::common::parse_relations(body["content"]);
-
-        auto payload = olm::client()->encrypt_group_message(session.get(), body.dump());
-
-        // Prepare the m.room.encrypted event.
-        msg::Encrypted data;
-        data.ciphertext = std::string((char *)payload.data(), payload.size());
-        data.sender_key = olm::client()->identity_keys().curve25519;
-        data.session_id = mtx::crypto::session_id(session.get());
-        data.device_id  = device_id;
-        data.algorithm  = MEGOLM_ALGO;
-        data.relations  = relations;
-
-        group_session_data.message_index = olm_outbound_group_session_message_index(session.get());
-        nhlog::crypto()->debug("next message_index {}", group_session_data.message_index);
-
-        // update current set of members for the session with the new members and that message_index
-        for (const auto &[user, devices] : sendSessionTo) {
-                if (!group_session_data.currently.keys.count(user))
-                        group_session_data.currently.keys[user] = {};
-
-                for (const auto &device_id_ : devices) {
-                        if (!group_session_data.currently.keys[user].deviceids.count(device_id_))
-                                group_session_data.currently.keys[user].deviceids[device_id_] =
-                                  group_session_data.message_index;
-                }
-        }
-
-        // We need to re-pickle the session after we send a message to save the new message_index.
-        cache::updateOutboundMegolmSession(room_id, group_session_data, session);
-
-        return data;
-}
-
-nlohmann::json
-try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &msg)
-{
-        auto session_ids = cache::getOlmSessions(sender_key);
-
-        nhlog::crypto()->info("attempt to decrypt message with {} known session_ids",
-                              session_ids.size());
-
-        for (const auto &id : session_ids) {
-                auto session = cache::getOlmSession(sender_key, id);
-
-                if (!session) {
-                        nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id);
-                        continue;
-                }
-
-                mtx::crypto::BinaryBuf text;
-
-                try {
-                        text = olm::client()->decrypt_message(session->get(), msg.type, msg.body);
-                        nhlog::crypto()->debug("Updated olm session: {}",
-                                               mtx::crypto::session_id(session->get()));
-                        cache::saveOlmSession(
-                          id, std::move(session.value()), QDateTime::currentMSecsSinceEpoch());
-                } catch (const mtx::crypto::olm_exception &e) {
-                        nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}",
-                                               msg.type,
-                                               sender_key,
-                                               id,
-                                               e.what());
-                        continue;
-                } catch (const lmdb::error &e) {
-                        nhlog::crypto()->critical("failed to save session: {}", e.what());
-                        return {};
-                }
-
-                try {
-                        return json::parse(std::string_view((char *)text.data(), text.size()));
-                } catch (const json::exception &e) {
-                        nhlog::crypto()->critical(
-                          "failed to parse the decrypted session msg: {} {}",
-                          e.what(),
-                          std::string_view((char *)text.data(), text.size()));
-                }
-        }
-
-        return {};
-}
-
-void
-create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
-                              const std::string &sender_key,
-                              const std::string &sender_ed25519)
-{
-        MegolmSessionIndex index;
-        index.room_id    = roomKey.content.room_id;
-        index.session_id = roomKey.content.session_id;
-        index.sender_key = sender_key;
-
-        try {
-                GroupSessionData data{};
-                data.forwarding_curve25519_key_chain = {sender_key};
-                data.sender_claimed_ed25519_key      = sender_ed25519;
-
-                auto megolm_session =
-                  olm::client()->init_inbound_group_session(roomKey.content.session_key);
-                cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
-        } catch (const lmdb::error &e) {
-                nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
-                return;
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what());
-                return;
-        }
-
-        nhlog::crypto()->info(
-          "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
-
-        ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
-}
-
-void
-import_inbound_megolm_session(
-  const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey)
-{
-        MegolmSessionIndex index;
-        index.room_id    = roomKey.content.room_id;
-        index.session_id = roomKey.content.session_id;
-        index.sender_key = roomKey.content.sender_key;
-
-        try {
-                auto megolm_session =
-                  olm::client()->import_inbound_group_session(roomKey.content.session_key);
-
-                GroupSessionData data{};
-                data.forwarding_curve25519_key_chain =
-                  roomKey.content.forwarding_curve25519_key_chain;
-                data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key;
-
-                cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
-        } catch (const lmdb::error &e) {
-                nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
-                return;
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what());
-                return;
-        }
-
-        nhlog::crypto()->info(
-          "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
-
-        ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
-}
-
-void
-mark_keys_as_published()
-{
-        olm::client()->mark_keys_as_published();
-        cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY));
-}
-
-void
-send_key_request_for(mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> e,
-                     const std::string &request_id,
-                     bool cancel)
-{
-        using namespace mtx::events;
-
-        nhlog::crypto()->debug("sending key request: sender_key {}, session_id {}",
-                               e.content.sender_key,
-                               e.content.session_id);
-
-        mtx::events::msg::KeyRequest request;
-        request.action = cancel ? mtx::events::msg::RequestAction::Cancellation
-                                : mtx::events::msg::RequestAction::Request;
-
-        request.algorithm            = MEGOLM_ALGO;
-        request.room_id              = e.room_id;
-        request.sender_key           = e.content.sender_key;
-        request.session_id           = e.content.session_id;
-        request.request_id           = request_id;
-        request.requesting_device_id = http::client()->device_id();
-
-        nhlog::crypto()->debug("m.room_key_request: {}", json(request).dump(2));
-
-        std::map<mtx::identifiers::User, std::map<std::string, decltype(request)>> body;
-        body[mtx::identifiers::parse<mtx::identifiers::User>(e.sender)][e.content.device_id] =
-          request;
-        body[http::client()->user_id()]["*"] = request;
-
-        http::client()->send_to_device(
-          http::client()->generate_txn_id(), body, [e](mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to send "
-                                             "send_to_device "
-                                             "message: {}",
-                                             err->matrix_error.error);
-                  }
-
-                  nhlog::net()->info("m.room_key_request sent to {}:{} and your own devices",
-                                     e.sender,
-                                     e.content.device_id);
-          });
-}
-
-void
-handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> &req)
-{
-        if (req.content.algorithm != MEGOLM_ALGO) {
-                nhlog::crypto()->debug("ignoring key request {} with invalid algorithm: {}",
-                                       req.content.request_id,
-                                       req.content.algorithm);
-                return;
-        }
-
-        // Check if we were the sender of the session being requested (unless it is actually us
-        // requesting the session).
-        if (req.sender != http::client()->user_id().to_string() &&
-            req.content.sender_key != olm::client()->identity_keys().curve25519) {
-                nhlog::crypto()->debug(
-                  "ignoring key request {} because we did not create the requested session: "
-                  "\nrequested({}) ours({})",
-                  req.content.request_id,
-                  req.content.sender_key,
-                  olm::client()->identity_keys().curve25519);
-                return;
-        }
-
-        // Check that the requested session_id and the one we have saved match.
-        MegolmSessionIndex index{};
-        index.room_id    = req.content.room_id;
-        index.session_id = req.content.session_id;
-        index.sender_key = req.content.sender_key;
-
-        // Check if we have the keys for the requested session.
-        auto sessionData = cache::getMegolmSessionData(index);
-        if (!sessionData) {
-                nhlog::crypto()->warn("requested session not found in room: {}",
-                                      req.content.room_id);
-                return;
-        }
-
-        const auto session = cache::getInboundMegolmSession(index);
-        if (!session) {
-                nhlog::crypto()->warn("No session with id {} in db", req.content.session_id);
-                return;
-        }
-
-        if (!cache::isRoomMember(req.sender, req.content.room_id)) {
-                nhlog::crypto()->warn(
-                  "user {} that requested the session key is not member of the room {}",
-                  req.sender,
-                  req.content.room_id);
-                return;
-        }
-
-        // check if device is verified
-        auto verificationStatus = cache::verificationStatus(req.sender);
-        bool verifiedDevice     = false;
-        if (verificationStatus &&
-            // Share keys, if the option to share with trusted users is enabled or with yourself
-            (ChatPage::instance()->userSettings()->shareKeysWithTrustedUsers() ||
-             req.sender == http::client()->user_id().to_string())) {
-                for (const auto &dev : verificationStatus->verified_devices) {
-                        if (dev == req.content.requesting_device_id) {
-                                verifiedDevice = true;
-                                nhlog::crypto()->debug("Verified device: {}", dev);
-                                break;
-                        }
-                }
-        }
-
-        bool shouldSeeKeys    = false;
-        uint64_t minimumIndex = -1;
-        if (sessionData->currently.keys.count(req.sender)) {
-                if (sessionData->currently.keys.at(req.sender)
-                      .deviceids.count(req.content.requesting_device_id)) {
-                        shouldSeeKeys = true;
-                        minimumIndex  = sessionData->currently.keys.at(req.sender)
-                                         .deviceids.at(req.content.requesting_device_id);
-                }
-        }
-
-        if (!verifiedDevice && !shouldSeeKeys) {
-                nhlog::crypto()->debug("ignoring key request for room {}", req.content.room_id);
-                return;
-        }
-
-        if (verifiedDevice) {
-                // share the minimum index we have
-                minimumIndex = -1;
-        }
-
-        try {
-                auto session_key = mtx::crypto::export_session(session.get(), minimumIndex);
-
-                //
-                // Prepare the m.room_key event.
-                //
-                mtx::events::msg::ForwardedRoomKey forward_key{};
-                forward_key.algorithm   = MEGOLM_ALGO;
-                forward_key.room_id     = index.room_id;
-                forward_key.session_id  = index.session_id;
-                forward_key.session_key = session_key;
-                forward_key.sender_key  = index.sender_key;
-
-                // TODO(Nico): Figure out if this is correct
-                forward_key.sender_claimed_ed25519_key = sessionData->sender_claimed_ed25519_key;
-                forward_key.forwarding_curve25519_key_chain =
-                  sessionData->forwarding_curve25519_key_chain;
-
-                send_megolm_key_to_device(
-                  req.sender, req.content.requesting_device_id, forward_key);
-        } catch (std::exception &e) {
-                nhlog::crypto()->error("Failed to forward session key: {}", e.what());
-        }
-}
-
-void
-send_megolm_key_to_device(const std::string &user_id,
-                          const std::string &device_id,
-                          const mtx::events::msg::ForwardedRoomKey &payload)
-{
-        mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> room_key;
-        room_key.content = payload;
-        room_key.type    = mtx::events::EventType::ForwardedRoomKey;
-
-        std::map<std::string, std::vector<std::string>> targets;
-        targets[user_id] = {device_id};
-        send_encrypted_to_device_messages(targets, room_key);
-        nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id);
-}
-
-DecryptionResult
-decryptEvent(const MegolmSessionIndex &index,
-             const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event,
-             bool dont_write_db)
-{
-        try {
-                if (!cache::client()->inboundMegolmSessionExists(index)) {
-                        return {DecryptionErrorCode::MissingSession, std::nullopt, std::nullopt};
-                }
-        } catch (const lmdb::error &e) {
-                return {DecryptionErrorCode::DbError, e.what(), std::nullopt};
-        }
-
-        // TODO: Lookup index,event_id,origin_server_ts tuple for replay attack errors
-
-        std::string msg_str;
-        try {
-                auto session = cache::client()->getInboundMegolmSession(index);
-                auto sessionData =
-                  cache::client()->getMegolmSessionData(index).value_or(GroupSessionData{});
-
-                auto res =
-                  olm::client()->decrypt_group_message(session.get(), event.content.ciphertext);
-                msg_str = std::string((char *)res.data.data(), res.data.size());
-
-                if (!event.event_id.empty() && event.event_id[0] == '$') {
-                        auto oldIdx = sessionData.indices.find(res.message_index);
-                        if (oldIdx != sessionData.indices.end()) {
-                                if (oldIdx->second != event.event_id)
-                                        return {DecryptionErrorCode::ReplayAttack,
-                                                std::nullopt,
-                                                std::nullopt};
-                        } else if (!dont_write_db) {
-                                sessionData.indices[res.message_index] = event.event_id;
-                                cache::client()->saveInboundMegolmSession(
-                                  index, std::move(session), sessionData);
-                        }
-                }
-        } catch (const lmdb::error &e) {
-                return {DecryptionErrorCode::DbError, e.what(), std::nullopt};
-        } catch (const mtx::crypto::olm_exception &e) {
-                if (e.error_code() == mtx::crypto::OlmErrorCode::UNKNOWN_MESSAGE_INDEX)
-                        return {DecryptionErrorCode::MissingSessionIndex, e.what(), std::nullopt};
-                return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt};
-        }
-
-        try {
-                // Add missing fields for the event.
-                json body                = json::parse(msg_str);
-                body["event_id"]         = event.event_id;
-                body["sender"]           = event.sender;
-                body["origin_server_ts"] = event.origin_server_ts;
-                body["unsigned"]         = event.unsigned_data;
-
-                // relations are unencrypted in content...
-                mtx::common::add_relations(body["content"], event.content.relations);
-
-                mtx::events::collections::TimelineEvent te;
-                mtx::events::collections::from_json(body, te);
-
-                return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)};
-        } catch (std::exception &e) {
-                return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
-        }
-}
-
-crypto::Trust
-calculate_trust(const std::string &user_id, const std::string &curve25519)
-{
-        auto status              = cache::client()->verificationStatus(user_id);
-        crypto::Trust trustlevel = crypto::Trust::Unverified;
-        if (status.verified_device_keys.count(curve25519))
-                trustlevel = status.verified_device_keys.at(curve25519);
-
-        return trustlevel;
-}
-
-//! Send encrypted to device messages, targets is a map from userid to device ids or {} for all
-//! devices
-void
-send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::string>> targets,
-                                  const mtx::events::collections::DeviceEvents &event,
-                                  bool force_new_session)
-{
-        static QMap<QPair<std::string, std::string>, qint64> rateLimit;
-
-        nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event);
-
-        std::map<std::string, std::vector<std::string>> keysToQuery;
-        mtx::requests::ClaimKeys claims;
-        std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::OlmEncrypted>>
-          messages;
-        std::map<std::string, std::map<std::string, DevicePublicKeys>> pks;
-
-        auto our_curve = olm::client()->identity_keys().curve25519;
-
-        for (const auto &[user, devices] : targets) {
-                auto deviceKeys = cache::client()->userKeys(user);
-
-                // no keys for user, query them
-                if (!deviceKeys) {
-                        keysToQuery[user] = devices;
-                        continue;
-                }
-
-                auto deviceTargets = devices;
-                if (devices.empty()) {
-                        deviceTargets.clear();
-                        for (const auto &[device, keys] : deviceKeys->device_keys) {
-                                (void)keys;
-                                deviceTargets.push_back(device);
-                        }
-                }
-
-                for (const auto &device : deviceTargets) {
-                        if (!deviceKeys->device_keys.count(device)) {
-                                keysToQuery[user] = {};
-                                break;
-                        }
-
-                        auto d = deviceKeys->device_keys.at(device);
-
-                        if (!d.keys.count("curve25519:" + device) ||
-                            !d.keys.count("ed25519:" + device)) {
-                                nhlog::crypto()->warn("Skipping device {} since it has no keys!",
-                                                      device);
-                                continue;
-                        }
-
-                        auto device_curve = d.keys.at("curve25519:" + device);
-                        if (device_curve == our_curve) {
-                                nhlog::crypto()->warn("Skipping our own device, since sending "
-                                                      "ourselves olm messages makes no sense.");
-                                continue;
-                        }
-
-                        auto session = cache::getLatestOlmSession(device_curve);
-                        if (!session || force_new_session) {
-                                auto currentTime = QDateTime::currentSecsSinceEpoch();
-                                if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
-                                    currentTime) {
-                                        claims.one_time_keys[user][device] =
-                                          mtx::crypto::SIGNED_CURVE25519;
-                                        pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
-                                        pks[user][device].curve25519 =
-                                          d.keys.at("curve25519:" + device);
-
-                                        rateLimit.insert(QPair(user, device), currentTime);
-                                } else {
-                                        nhlog::crypto()->warn("Not creating new session with {}:{} "
-                                                              "because of rate limit",
-                                                              user,
-                                                              device);
-                                }
-                                continue;
-                        }
-
-                        messages[mtx::identifiers::parse<mtx::identifiers::User>(user)][device] =
-                          olm::client()
-                            ->create_olm_encrypted_content(session->get(),
-                                                           ev_json,
-                                                           UserId(user),
-                                                           d.keys.at("ed25519:" + device),
-                                                           device_curve)
-                            .get<mtx::events::msg::OlmEncrypted>();
-
-                        try {
-                                nhlog::crypto()->debug("Updated olm session: {}",
-                                                       mtx::crypto::session_id(session->get()));
-                                cache::saveOlmSession(d.keys.at("curve25519:" + device),
-                                                      std::move(*session),
-                                                      QDateTime::currentMSecsSinceEpoch());
-                        } 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());
-                        }
-                }
-        }
-
-        if (!messages.empty())
-                http::client()->send_to_device<mtx::events::msg::OlmEncrypted>(
-                  http::client()->generate_txn_id(), messages, [](mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::net()->warn("failed to send "
-                                                     "send_to_device "
-                                                     "message: {}",
-                                                     err->matrix_error.error);
-                          }
-                  });
-
-        auto BindPks = [ev_json](decltype(pks) pks_temp) {
-                return [pks = pks_temp, ev_json](const mtx::responses::ClaimKeys &res,
-                                                 mtx::http::RequestErr) {
-                        std::map<mtx::identifiers::User,
-                                 std::map<std::string, mtx::events::msg::OlmEncrypted>>
-                          messages;
-                        for (const auto &[user_id, retrieved_devices] : res.one_time_keys) {
-                                nhlog::net()->debug("claimed keys for {}", user_id);
-                                if (retrieved_devices.size() == 0) {
-                                        nhlog::net()->debug(
-                                          "no one-time keys found for user_id: {}", user_id);
-                                        continue;
-                                }
-
-                                for (const auto &rd : retrieved_devices) {
-                                        const auto device_id = rd.first;
-
-                                        nhlog::net()->debug(
-                                          "{} : \n {}", device_id, rd.second.dump(2));
-
-                                        if (rd.second.empty() ||
-                                            !rd.second.begin()->contains("key")) {
-                                                nhlog::net()->warn(
-                                                  "Skipping device {} as it has no key.",
-                                                  device_id);
-                                                continue;
-                                        }
-
-                                        auto otk = rd.second.begin()->at("key");
-
-                                        auto sign_key = pks.at(user_id).at(device_id).ed25519;
-                                        auto id_key   = pks.at(user_id).at(device_id).curve25519;
-
-                                        // Verify signature
-                                        {
-                                                auto signedKey = *rd.second.begin();
-                                                std::string signature =
-                                                  signedKey["signatures"][user_id].value(
-                                                    "ed25519:" + device_id, "");
-
-                                                if (signature.empty() ||
-                                                    !mtx::crypto::ed25519_verify_signature(
-                                                      sign_key, signedKey, signature)) {
-                                                        nhlog::net()->warn(
-                                                          "Skipping device {} as its one time key "
-                                                          "has an invalid signature.",
-                                                          device_id);
-                                                        continue;
-                                                }
-                                        }
-
-                                        auto session =
-                                          olm::client()->create_outbound_session(id_key, otk);
-
-                                        messages[mtx::identifiers::parse<mtx::identifiers::User>(
-                                          user_id)][device_id] =
-                                          olm::client()
-                                            ->create_olm_encrypted_content(session.get(),
-                                                                           ev_json,
-                                                                           UserId(user_id),
-                                                                           sign_key,
-                                                                           id_key)
-                                            .get<mtx::events::msg::OlmEncrypted>();
-
-                                        try {
-                                                nhlog::crypto()->debug(
-                                                  "Updated olm session: {}",
-                                                  mtx::crypto::session_id(session.get()));
-                                                cache::saveOlmSession(
-                                                  id_key,
-                                                  std::move(session),
-                                                  QDateTime::currentMSecsSinceEpoch());
-                                        } 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());
-                                        }
-                                }
-                                nhlog::net()->info("send_to_device: {}", user_id);
-                        }
-
-                        if (!messages.empty())
-                                http::client()->send_to_device<mtx::events::msg::OlmEncrypted>(
-                                  http::client()->generate_txn_id(),
-                                  messages,
-                                  [](mtx::http::RequestErr err) {
-                                          if (err) {
-                                                  nhlog::net()->warn("failed to send "
-                                                                     "send_to_device "
-                                                                     "message: {}",
-                                                                     err->matrix_error.error);
-                                          }
-                                  });
-                };
-        };
-
-        if (!claims.one_time_keys.empty())
-                http::client()->claim_keys(claims, BindPks(pks));
-
-        if (!keysToQuery.empty()) {
-                mtx::requests::QueryKeys req;
-                req.device_keys = keysToQuery;
-                http::client()->query_keys(
-                  req,
-                  [ev_json, BindPks, our_curve](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));
-                                  return;
-                          }
-
-                          nhlog::net()->info("queried keys");
-
-                          cache::client()->updateUserKeys(cache::nextBatchToken(), res);
-
-                          mtx::requests::ClaimKeys claim_keys;
-
-                          std::map<std::string, std::map<std::string, DevicePublicKeys>> deviceKeys;
-
-                          for (const auto &user : res.device_keys) {
-                                  for (const auto &dev : user.second) {
-                                          const auto user_id   = ::UserId(dev.second.user_id);
-                                          const auto device_id = DeviceId(dev.second.device_id);
-
-                                          if (user_id.get() ==
-                                                http::client()->user_id().to_string() &&
-                                              device_id.get() == http::client()->device_id())
-                                                  continue;
-
-                                          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);
-
-                                          if (pks.curve25519 == our_curve) {
-                                                  nhlog::crypto()->warn(
-                                                    "Skipping our own device, since sending "
-                                                    "ourselves olm messages makes no sense.");
-                                                  continue;
-                                          }
-
-                                          try {
-                                                  if (!mtx::crypto::verify_identity_signature(
-                                                        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 currentTime = QDateTime::currentSecsSinceEpoch();
-                                          if (rateLimit.value(QPair(user.first, device_id.get())) +
-                                                60 * 60 * 10 <
-                                              currentTime) {
-                                                  deviceKeys[user_id].emplace(device_id, pks);
-                                                  claim_keys.one_time_keys[user.first][device_id] =
-                                                    mtx::crypto::SIGNED_CURVE25519;
-
-                                                  rateLimit.insert(
-                                                    QPair(user.first, device_id.get()),
-                                                    currentTime);
-                                          } else {
-                                                  nhlog::crypto()->warn(
-                                                    "Not creating new session with {}:{} "
-                                                    "because of rate limit",
-                                                    user.first,
-                                                    device_id.get());
-                                                  continue;
-                                          }
-
-                                          nhlog::net()->info("{}", device_id.get());
-                                          nhlog::net()->info("  curve25519 {}", pks.curve25519);
-                                          nhlog::net()->info("  ed25519 {}", pks.ed25519);
-                                  }
-                          }
-
-                          if (!claim_keys.one_time_keys.empty())
-                                  http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
-                  });
-        }
-}
-
-void
-request_cross_signing_keys()
-{
-        mtx::events::msg::SecretRequest secretRequest{};
-        secretRequest.action               = mtx::events::msg::RequestAction::Request;
-        secretRequest.requesting_device_id = http::client()->device_id();
-
-        auto local_user = http::client()->user_id();
-
-        auto verificationStatus = cache::verificationStatus(local_user.to_string());
-
-        if (!verificationStatus)
-                return;
-
-        auto request = [&](std::string secretName) {
-                secretRequest.name       = secretName;
-                secretRequest.request_id = "ss." + http::client()->generate_txn_id();
-
-                request_id_to_secret_name[secretRequest.request_id] = secretRequest.name;
-
-                std::map<mtx::identifiers::User,
-                         std::map<std::string, mtx::events::msg::SecretRequest>>
-                  body;
-
-                for (const auto &dev : verificationStatus->verified_devices) {
-                        if (dev != secretRequest.requesting_device_id)
-                                body[local_user][dev] = secretRequest;
-                }
-
-                http::client()->send_to_device<mtx::events::msg::SecretRequest>(
-                  http::client()->generate_txn_id(),
-                  body,
-                  [request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::net()->error("Failed to send request for secrect '{}'",
-                                                      secretName);
-                                  // Cancel request on UI thread
-                                  QTimer::singleShot(1, cache::client(), [request_id]() {
-                                          request_id_to_secret_name.erase(request_id);
-                                  });
-                                  return;
-                          }
-                  });
-
-                for (const auto &dev : verificationStatus->verified_devices) {
-                        if (dev != secretRequest.requesting_device_id)
-                                body[local_user][dev].action =
-                                  mtx::events::msg::RequestAction::Cancellation;
-                }
-
-                // timeout after 15 min
-                QTimer::singleShot(15 * 60 * 1000, [secretRequest, body]() {
-                        if (request_id_to_secret_name.count(secretRequest.request_id)) {
-                                request_id_to_secret_name.erase(secretRequest.request_id);
-                                http::client()->send_to_device<mtx::events::msg::SecretRequest>(
-                                  http::client()->generate_txn_id(),
-                                  body,
-                                  [secretRequest](mtx::http::RequestErr err) {
-                                          if (err) {
-                                                  nhlog::net()->error(
-                                                    "Failed to cancel request for secrect '{}'",
-                                                    secretRequest.name);
-                                                  return;
-                                          }
-                                  });
-                        }
-                });
-        };
-
-        request(mtx::secret_storage::secrets::cross_signing_self_signing);
-        request(mtx::secret_storage::secrets::cross_signing_user_signing);
-        request(mtx::secret_storage::secrets::megolm_backup_v1);
-}
-
-namespace {
-void
-unlock_secrets(const std::string &key,
-               const std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData> &secrets)
-{
-        http::client()->secret_storage_key(
-          key,
-          [secrets](mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
-                    mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->error("Failed to download secret storage key");
-                          return;
-                  }
-
-                  emit ChatPage::instance()->downloadedSecrets(keyDesc, secrets);
-          });
-}
-}
-
-void
-download_cross_signing_keys()
-{
-        using namespace mtx::secret_storage;
-        http::client()->secret_storage_secret(
-          secrets::megolm_backup_v1, [](Secret secret, mtx::http::RequestErr err) {
-                  std::optional<Secret> backup_key;
-                  if (!err)
-                          backup_key = secret;
-
-                  http::client()->secret_storage_secret(
-                    secrets::cross_signing_self_signing,
-                    [backup_key](Secret secret, mtx::http::RequestErr err) {
-                            std::optional<Secret> self_signing_key;
-                            if (!err)
-                                    self_signing_key = secret;
-
-                            http::client()->secret_storage_secret(
-                              secrets::cross_signing_user_signing,
-                              [backup_key, self_signing_key](Secret secret,
-                                                             mtx::http::RequestErr err) {
-                                      std::optional<Secret> user_signing_key;
-                                      if (!err)
-                                              user_signing_key = secret;
-
-                                      std::map<std::string,
-                                               std::map<std::string, AesHmacSha2EncryptedData>>
-                                        secrets;
-
-                                      if (backup_key && !backup_key->encrypted.empty())
-                                              secrets[backup_key->encrypted.begin()->first]
-                                                     [secrets::megolm_backup_v1] =
-                                                       backup_key->encrypted.begin()->second;
-                                      if (self_signing_key && !self_signing_key->encrypted.empty())
-                                              secrets[self_signing_key->encrypted.begin()->first]
-                                                     [secrets::cross_signing_self_signing] =
-                                                       self_signing_key->encrypted.begin()->second;
-                                      if (user_signing_key && !user_signing_key->encrypted.empty())
-                                              secrets[user_signing_key->encrypted.begin()->first]
-                                                     [secrets::cross_signing_user_signing] =
-                                                       user_signing_key->encrypted.begin()->second;
-
-                                      for (const auto &[key, secrets] : secrets)
-                                              unlock_secrets(key, secrets);
-                              });
-                    });
-          });
-}
-
-} // namespace olm
diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp
index 25262c59fd37c998f39d3858e2dec02ac03b1c01..ff93f7d83000ed5b623cbafc7f9cc6d775068e34 100644
--- a/src/ReadReceiptsModel.cpp
+++ b/src/ReadReceiptsModel.cpp
@@ -16,116 +16,115 @@ ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject
   , event_id_{event_id}
   , room_id_{room_id}
 {
-        try {
-                addUsers(cache::readReceipts(event_id_, room_id_));
-        } catch (const lmdb::error &) {
-                nhlog::db()->warn("failed to retrieve read receipts for {} {}",
-                                  event_id_.toStdString(),
-                                  room_id_.toStdString());
-
-                return;
-        }
+    try {
+        addUsers(cache::readReceipts(event_id_, room_id_));
+    } catch (const lmdb::error &) {
+        nhlog::db()->warn("failed to retrieve read receipts for {} {}",
+                          event_id_.toStdString(),
+                          room_id_.toStdString());
+
+        return;
+    }
 
-        connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update);
+    connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update);
 }
 
 void
 ReadReceiptsModel::update()
 {
-        try {
-                addUsers(cache::readReceipts(event_id_, room_id_));
-        } catch (const lmdb::error &) {
-                nhlog::db()->warn("failed to retrieve read receipts for {} {}",
-                                  event_id_.toStdString(),
-                                  room_id_.toStdString());
-
-                return;
-        }
+    try {
+        addUsers(cache::readReceipts(event_id_, room_id_));
+    } catch (const lmdb::error &) {
+        nhlog::db()->warn("failed to retrieve read receipts for {} {}",
+                          event_id_.toStdString(),
+                          room_id_.toStdString());
+
+        return;
+    }
 }
 
 QHash<int, QByteArray>
 ReadReceiptsModel::roleNames() const
 {
-        // Note: RawTimestamp is purposely not included here
-        return {
-          {Mxid, "mxid"},
-          {DisplayName, "displayName"},
-          {AvatarUrl, "avatarUrl"},
-          {Timestamp, "timestamp"},
-        };
+    // Note: RawTimestamp is purposely not included here
+    return {
+      {Mxid, "mxid"},
+      {DisplayName, "displayName"},
+      {AvatarUrl, "avatarUrl"},
+      {Timestamp, "timestamp"},
+    };
 }
 
 QVariant
 ReadReceiptsModel::data(const QModelIndex &index, int role) const
 {
-        if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0)
-                return {};
-
-        switch (role) {
-        case Mxid:
-                return readReceipts_[index.row()].first;
-        case DisplayName:
-                return cache::displayName(room_id_, readReceipts_[index.row()].first);
-        case AvatarUrl:
-                return cache::avatarUrl(room_id_, readReceipts_[index.row()].first);
-        case Timestamp:
-                return dateFormat(readReceipts_[index.row()].second);
-        case RawTimestamp:
-                return readReceipts_[index.row()].second;
-        default:
-                return {};
-        }
+    if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0)
+        return {};
+
+    switch (role) {
+    case Mxid:
+        return readReceipts_[index.row()].first;
+    case DisplayName:
+        return cache::displayName(room_id_, readReceipts_[index.row()].first);
+    case AvatarUrl:
+        return cache::avatarUrl(room_id_, readReceipts_[index.row()].first);
+    case Timestamp:
+        return dateFormat(readReceipts_[index.row()].second);
+    case RawTimestamp:
+        return readReceipts_[index.row()].second;
+    default:
+        return {};
+    }
 }
 
 void
 ReadReceiptsModel::addUsers(
   const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users)
 {
-        auto newReceipts = users.size() - readReceipts_.size();
-
-        if (newReceipts > 0) {
-                beginInsertRows(
-                  QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1);
+    auto newReceipts = users.size() - readReceipts_.size();
 
-                for (const auto &user : users) {
-                        QPair<QString, QDateTime> item = {
-                          QString::fromStdString(user.second),
-                          QDateTime::fromMSecsSinceEpoch(user.first)};
-                        if (!readReceipts_.contains(item))
-                                readReceipts_.push_back(item);
-                }
+    if (newReceipts > 0) {
+        beginInsertRows(
+          QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1);
 
-                endInsertRows();
+        for (const auto &user : users) {
+            QPair<QString, QDateTime> item = {QString::fromStdString(user.second),
+                                              QDateTime::fromMSecsSinceEpoch(user.first)};
+            if (!readReceipts_.contains(item))
+                readReceipts_.push_back(item);
         }
+
+        endInsertRows();
+    }
 }
 
 QString
 ReadReceiptsModel::dateFormat(const QDateTime &then) const
 {
-        auto now  = QDateTime::currentDateTime();
-        auto days = then.daysTo(now);
-
-        if (days == 0)
-                return QLocale::system().toString(then.time(), QLocale::ShortFormat);
-        else if (days < 2)
-                return tr("Yesterday, %1")
-                  .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
-        else if (days < 7)
-                //: %1 is the name of the current day, %2 is the time the read receipt was read. The
-                //: result may look like this: Monday, 7:15
-                return QString("%1, %2")
-                  .arg(then.toString("dddd"))
-                  .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
+    auto now  = QDateTime::currentDateTime();
+    auto days = then.daysTo(now);
 
+    if (days == 0)
         return QLocale::system().toString(then.time(), QLocale::ShortFormat);
+    else if (days < 2)
+        return tr("Yesterday, %1")
+          .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
+    else if (days < 7)
+        //: %1 is the name of the current day, %2 is the time the read receipt was read. The
+        //: result may look like this: Monday, 7:15
+        return QString("%1, %2")
+          .arg(then.toString("dddd"))
+          .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
+
+    return QLocale::system().toString(then.time(), QLocale::ShortFormat);
 }
 
 ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent)
   : QSortFilterProxyModel{parent}
   , model_{event_id, room_id, this}
 {
-        setSourceModel(&model_);
-        setSortRole(ReadReceiptsModel::RawTimestamp);
-        sort(0, Qt::DescendingOrder);
-        setDynamicSortFilter(true);
+    setSourceModel(&model_);
+    setSortRole(ReadReceiptsModel::RawTimestamp);
+    sort(0, Qt::DescendingOrder);
+    setDynamicSortFilter(true);
 }
diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h
index 3b45716c555d47a541635644f1fbfa6562344011..7487fff8bf2fd7e457205f4723391ed005826c75 100644
--- a/src/ReadReceiptsModel.h
+++ b/src/ReadReceiptsModel.h
@@ -13,61 +13,61 @@
 
 class ReadReceiptsModel : public QAbstractListModel
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        enum Roles
-        {
-                Mxid,
-                DisplayName,
-                AvatarUrl,
-                Timestamp,
-                RawTimestamp,
-        };
-
-        explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr);
-
-        QString eventId() const { return event_id_; }
-        QString roomId() const { return room_id_; }
-
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent) const override
-        {
-                Q_UNUSED(parent)
-                return readReceipts_.size();
-        }
-        QVariant data(const QModelIndex &index, int role) const override;
+    enum Roles
+    {
+        Mxid,
+        DisplayName,
+        AvatarUrl,
+        Timestamp,
+        RawTimestamp,
+    };
+
+    explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr);
+
+    QString eventId() const { return event_id_; }
+    QString roomId() const { return room_id_; }
+
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent) const override
+    {
+        Q_UNUSED(parent)
+        return readReceipts_.size();
+    }
+    QVariant data(const QModelIndex &index, int role) const override;
 
 public slots:
-        void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
-        void update();
+    void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
+    void update();
 
 private:
-        QString dateFormat(const QDateTime &then) const;
+    QString dateFormat(const QDateTime &then) const;
 
-        QString event_id_;
-        QString room_id_;
-        QVector<QPair<QString, QDateTime>> readReceipts_;
+    QString event_id_;
+    QString room_id_;
+    QVector<QPair<QString, QDateTime>> readReceipts_;
 };
 
 class ReadReceiptsProxy : public QSortFilterProxyModel
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QString eventId READ eventId CONSTANT)
-        Q_PROPERTY(QString roomId READ roomId CONSTANT)
+    Q_PROPERTY(QString eventId READ eventId CONSTANT)
+    Q_PROPERTY(QString roomId READ roomId CONSTANT)
 
 public:
-        explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr);
+    explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr);
 
-        QString eventId() const { return event_id_; }
-        QString roomId() const { return room_id_; }
+    QString eventId() const { return event_id_; }
+    QString roomId() const { return room_id_; }
 
 private:
-        QString event_id_;
-        QString room_id_;
+    QString event_id_;
+    QString room_id_;
 
-        ReadReceiptsModel model_;
+    ReadReceiptsModel model_;
 };
 
 #endif // READRECEIPTSMODEL_H
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index fb6a1b971bc1c34875ae91f6cffd19d95094614d..271a7fc2cfec711e63cb67cab238a3d1186fdf6b 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -23,6 +23,7 @@
 #include "ui/FlatButton.h"
 #include "ui/RaisedButton.h"
 #include "ui/TextField.h"
+#include "ui/UIA.h"
 
 #include "dialogs/FallbackAuth.h"
 #include "dialogs/ReCaptcha.h"
@@ -33,496 +34,379 @@ Q_DECLARE_METATYPE(mtx::user_interactive::Auth)
 RegisterPage::RegisterPage(QWidget *parent)
   : QWidget(parent)
 {
-        qRegisterMetaType<mtx::user_interactive::Unauthorized>();
-        qRegisterMetaType<mtx::user_interactive::Auth>();
-        top_layout_ = new QVBoxLayout();
-
-        back_layout_ = new QHBoxLayout();
-        back_layout_->setSpacing(0);
-        back_layout_->setContentsMargins(5, 5, -1, -1);
-
-        back_button_ = new FlatButton(this);
-        back_button_->setMinimumSize(QSize(30, 30));
-
-        QIcon icon;
-        icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png");
-
-        back_button_->setIcon(icon);
-        back_button_->setIconSize(QSize(32, 32));
-
-        back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
-        back_layout_->addStretch(1);
-
-        QIcon logo;
-        logo.addFile(":/logos/register.png");
-
-        logo_ = new QLabel(this);
-        logo_->setPixmap(logo.pixmap(128));
-
-        logo_layout_ = new QHBoxLayout();
-        logo_layout_->setMargin(0);
-        logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
-
-        form_wrapper_ = new QHBoxLayout();
-        form_widget_  = new QWidget();
-        form_widget_->setMinimumSize(QSize(350, 300));
-
-        form_layout_ = new QVBoxLayout();
-        form_layout_->setSpacing(20);
-        form_layout_->setContentsMargins(0, 0, 0, 40);
-        form_widget_->setLayout(form_layout_);
-
-        form_wrapper_->addStretch(1);
-        form_wrapper_->addWidget(form_widget_);
-        form_wrapper_->addStretch(1);
-
-        username_input_ = new TextField();
-        username_input_->setLabel(tr("Username"));
-        username_input_->setRegexp(QRegularExpression("[a-z0-9._=/-]+"));
-        username_input_->setToolTip(tr("The username must not be empty, and must contain only the "
-                                       "characters a-z, 0-9, ., _, =, -, and /."));
-
-        password_input_ = new TextField();
-        password_input_->setLabel(tr("Password"));
-        password_input_->setRegexp(QRegularExpression("^.{8,}$"));
-        password_input_->setEchoMode(QLineEdit::Password);
-        password_input_->setToolTip(tr("Please choose a secure password. The exact requirements "
-                                       "for password strength may depend on your server."));
-
-        password_confirmation_ = new TextField();
-        password_confirmation_->setLabel(tr("Password confirmation"));
-        password_confirmation_->setEchoMode(QLineEdit::Password);
-
-        server_input_ = new TextField();
-        server_input_->setLabel(tr("Homeserver"));
-        server_input_->setRegexp(QRegularExpression(".+"));
-        server_input_->setToolTip(
-          tr("A server that allows registration. Since matrix is decentralized, you need to first "
-             "find a server you can register on or host your own."));
-
-        error_username_label_ = new QLabel(this);
-        error_username_label_->setWordWrap(true);
-        error_username_label_->hide();
-
-        error_password_label_ = new QLabel(this);
-        error_password_label_->setWordWrap(true);
-        error_password_label_->hide();
-
-        error_password_confirmation_label_ = new QLabel(this);
-        error_password_confirmation_label_->setWordWrap(true);
-        error_password_confirmation_label_->hide();
-
-        error_server_label_ = new QLabel(this);
-        error_server_label_->setWordWrap(true);
-        error_server_label_->hide();
-
-        form_layout_->addWidget(username_input_, Qt::AlignHCenter);
-        form_layout_->addWidget(error_username_label_, Qt::AlignHCenter);
-        form_layout_->addWidget(password_input_, Qt::AlignHCenter);
-        form_layout_->addWidget(error_password_label_, Qt::AlignHCenter);
-        form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter);
-        form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter);
-        form_layout_->addWidget(server_input_, Qt::AlignHCenter);
-        form_layout_->addWidget(error_server_label_, Qt::AlignHCenter);
-
-        button_layout_ = new QHBoxLayout();
-        button_layout_->setSpacing(0);
-        button_layout_->setMargin(0);
-
-        error_label_ = new QLabel(this);
-        error_label_->setWordWrap(true);
-
-        register_button_ = new RaisedButton(tr("REGISTER"), this);
-        register_button_->setMinimumSize(350, 65);
-        register_button_->setFontSize(conf::btn::fontSize);
-        register_button_->setCornerRadius(conf::btn::cornerRadius);
-
-        button_layout_->addStretch(1);
-        button_layout_->addWidget(register_button_);
-        button_layout_->addStretch(1);
-
-        top_layout_->addLayout(back_layout_);
-        top_layout_->addLayout(logo_layout_);
-        top_layout_->addLayout(form_wrapper_);
-        top_layout_->addStretch(1);
-        top_layout_->addLayout(button_layout_);
-        top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
-        top_layout_->addStretch(1);
-        setLayout(top_layout_);
-
-        connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
-        connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
-
-        connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
-        connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
-        connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(password_confirmation_,
-                &TextField::editingFinished,
-                this,
-                &RegisterPage::checkPasswordConfirmation);
-        connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
-
-        connect(
-          this,
-          &RegisterPage::serverError,
-          this,
-          [this](const QString &msg) {
-                  server_input_->setValid(false);
-                  showError(error_server_label_, msg);
-          },
-          Qt::QueuedConnection);
-
-        connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
-        connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
-        connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
-        connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA);
-        connect(
-          this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth);
+    qRegisterMetaType<mtx::user_interactive::Unauthorized>();
+    qRegisterMetaType<mtx::user_interactive::Auth>();
+    top_layout_ = new QVBoxLayout();
+
+    back_layout_ = new QHBoxLayout();
+    back_layout_->setSpacing(0);
+    back_layout_->setContentsMargins(5, 5, -1, -1);
+
+    back_button_ = new FlatButton(this);
+    back_button_->setMinimumSize(QSize(30, 30));
+
+    QIcon icon;
+    icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png");
+
+    back_button_->setIcon(icon);
+    back_button_->setIconSize(QSize(32, 32));
+
+    back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
+    back_layout_->addStretch(1);
+
+    QIcon logo;
+    logo.addFile(":/logos/register.png");
+
+    logo_ = new QLabel(this);
+    logo_->setPixmap(logo.pixmap(128));
+
+    logo_layout_ = new QHBoxLayout();
+    logo_layout_->setMargin(0);
+    logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
+
+    form_wrapper_ = new QHBoxLayout();
+    form_widget_  = new QWidget();
+    form_widget_->setMinimumSize(QSize(350, 300));
+
+    form_layout_ = new QVBoxLayout();
+    form_layout_->setSpacing(20);
+    form_layout_->setContentsMargins(0, 0, 0, 40);
+    form_widget_->setLayout(form_layout_);
+
+    form_wrapper_->addStretch(1);
+    form_wrapper_->addWidget(form_widget_);
+    form_wrapper_->addStretch(1);
+
+    username_input_ = new TextField();
+    username_input_->setLabel(tr("Username"));
+    username_input_->setRegexp(QRegularExpression("[a-z0-9._=/-]+"));
+    username_input_->setToolTip(tr("The username must not be empty, and must contain only the "
+                                   "characters a-z, 0-9, ., _, =, -, and /."));
+
+    password_input_ = new TextField();
+    password_input_->setLabel(tr("Password"));
+    password_input_->setRegexp(QRegularExpression("^.{8,}$"));
+    password_input_->setEchoMode(QLineEdit::Password);
+    password_input_->setToolTip(tr("Please choose a secure password. The exact requirements "
+                                   "for password strength may depend on your server."));
+
+    password_confirmation_ = new TextField();
+    password_confirmation_->setLabel(tr("Password confirmation"));
+    password_confirmation_->setEchoMode(QLineEdit::Password);
+
+    server_input_ = new TextField();
+    server_input_->setLabel(tr("Homeserver"));
+    server_input_->setRegexp(QRegularExpression(".+"));
+    server_input_->setToolTip(
+      tr("A server that allows registration. Since matrix is decentralized, you need to first "
+         "find a server you can register on or host your own."));
+
+    error_username_label_ = new QLabel(this);
+    error_username_label_->setWordWrap(true);
+    error_username_label_->hide();
+
+    error_password_label_ = new QLabel(this);
+    error_password_label_->setWordWrap(true);
+    error_password_label_->hide();
+
+    error_password_confirmation_label_ = new QLabel(this);
+    error_password_confirmation_label_->setWordWrap(true);
+    error_password_confirmation_label_->hide();
+
+    error_server_label_ = new QLabel(this);
+    error_server_label_->setWordWrap(true);
+    error_server_label_->hide();
+
+    form_layout_->addWidget(username_input_, Qt::AlignHCenter);
+    form_layout_->addWidget(error_username_label_, Qt::AlignHCenter);
+    form_layout_->addWidget(password_input_, Qt::AlignHCenter);
+    form_layout_->addWidget(error_password_label_, Qt::AlignHCenter);
+    form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter);
+    form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter);
+    form_layout_->addWidget(server_input_, Qt::AlignHCenter);
+    form_layout_->addWidget(error_server_label_, Qt::AlignHCenter);
+
+    button_layout_ = new QHBoxLayout();
+    button_layout_->setSpacing(0);
+    button_layout_->setMargin(0);
+
+    error_label_ = new QLabel(this);
+    error_label_->setWordWrap(true);
+
+    register_button_ = new RaisedButton(tr("REGISTER"), this);
+    register_button_->setMinimumSize(350, 65);
+    register_button_->setFontSize(conf::btn::fontSize);
+    register_button_->setCornerRadius(conf::btn::cornerRadius);
+
+    button_layout_->addStretch(1);
+    button_layout_->addWidget(register_button_);
+    button_layout_->addStretch(1);
+
+    top_layout_->addLayout(back_layout_);
+    top_layout_->addLayout(logo_layout_);
+    top_layout_->addLayout(form_wrapper_);
+    top_layout_->addStretch(1);
+    top_layout_->addLayout(button_layout_);
+    top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
+    top_layout_->addStretch(1);
+    setLayout(top_layout_);
+
+    connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
+    connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
+
+    connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+    connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
+    connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+    connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
+    connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+    connect(password_confirmation_,
+            &TextField::editingFinished,
+            this,
+            &RegisterPage::checkPasswordConfirmation);
+    connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+    connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
+
+    connect(
+      this,
+      &RegisterPage::serverError,
+      this,
+      [this](const QString &msg) {
+          server_input_->setValid(false);
+          showError(error_server_label_, msg);
+      },
+      Qt::QueuedConnection);
+
+    connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
+    connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
+    connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
 }
 
 void
 RegisterPage::onBackButtonClicked()
 {
-        emit backButtonClicked();
+    emit backButtonClicked();
 }
 
 void
 RegisterPage::showError(const QString &msg)
 {
-        emit errorOccurred();
-        auto rect  = QFontMetrics(font()).boundingRect(msg);
-        int width  = rect.width();
-        int height = rect.height();
-        error_label_->setFixedHeight(qCeil(width / 200.0) * height);
-        error_label_->setText(msg);
+    emit errorOccurred();
+    auto rect  = QFontMetrics(font()).boundingRect(msg);
+    int width  = rect.width();
+    int height = rect.height();
+    error_label_->setFixedHeight(qCeil(width / 200.0) * height);
+    error_label_->setText(msg);
 }
 
 void
 RegisterPage::showError(QLabel *label, const QString &msg)
 {
-        emit errorOccurred();
-        auto rect  = QFontMetrics(font()).boundingRect(msg);
-        int width  = rect.width();
-        int height = rect.height();
-        label->setFixedHeight((int)qCeil(width / 200.0) * height);
-        label->setText(msg);
-        label->show();
+    emit errorOccurred();
+    auto rect  = QFontMetrics(font()).boundingRect(msg);
+    int width  = rect.width();
+    int height = rect.height();
+    label->setFixedHeight((int)qCeil(width / 200.0) * height);
+    label->setText(msg);
+    label->show();
 }
 
 bool
 RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
 {
-        if (t_field->isValid()) {
-                label->hide();
-                return true;
-        } else {
-                showError(label, msg);
-                return false;
-        }
+    if (t_field->isValid()) {
+        label->hide();
+        return true;
+    } else {
+        showError(label, msg);
+        return false;
+    }
 }
 
 bool
 RegisterPage::checkUsername()
 {
-        return checkOneField(error_username_label_,
-                             username_input_,
-                             tr("The username must not be empty, and must contain only the "
-                                "characters a-z, 0-9, ., _, =, -, and /."));
+    return checkOneField(error_username_label_,
+                         username_input_,
+                         tr("The username must not be empty, and must contain only the "
+                            "characters a-z, 0-9, ., _, =, -, and /."));
 }
 
 bool
 RegisterPage::checkPassword()
 {
-        return checkOneField(
-          error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
+    return checkOneField(
+      error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
 }
 
 bool
 RegisterPage::checkPasswordConfirmation()
 {
-        if (password_input_->text() == password_confirmation_->text()) {
-                error_password_confirmation_label_->hide();
-                password_confirmation_->setValid(true);
-                return true;
-        } else {
-                showError(error_password_confirmation_label_, tr("Passwords don't match"));
-                password_confirmation_->setValid(false);
-                return false;
-        }
+    if (password_input_->text() == password_confirmation_->text()) {
+        error_password_confirmation_label_->hide();
+        password_confirmation_->setValid(true);
+        return true;
+    } else {
+        showError(error_password_confirmation_label_, tr("Passwords don't match"));
+        password_confirmation_->setValid(false);
+        return false;
+    }
 }
 
 bool
 RegisterPage::checkServer()
 {
-        // This doesn't check that the server is reachable,
-        // just that the input is not obviously wrong.
-        return checkOneField(error_server_label_, server_input_, tr("Invalid server name"));
+    // This doesn't check that the server is reachable,
+    // just that the input is not obviously wrong.
+    return checkOneField(error_server_label_, server_input_, tr("Invalid server name"));
 }
 
 void
 RegisterPage::onRegisterButtonClicked()
 {
-        if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
-                auto server = server_input_->text().toStdString();
-
-                http::client()->set_server(server);
-                http::client()->verify_certificates(
-                  !UserSettings::instance()->disableCertificateValidation());
-
-                // This starts a chain of `emit`s which ends up doing the
-                // registration. Signals are used rather than normal function
-                // calls so that the dialogs used in UIA work correctly.
-                //
-                // The sequence of events looks something like this:
-                //
-                // dowellKnownLookup
-                //   v
-                // doVersionsCheck
-                //   v
-                // doRegistration
-                //   v
-                // doUIA <-----------------+
-                //   v					   | More auth required
-                // doRegistrationWithAuth -+
-                //                         | Success
-                // 						   v
-                //                     registering
-
-                emit wellKnownLookup();
-
-                emit registering();
-        }
+    if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
+        auto server = server_input_->text().toStdString();
+
+        http::client()->set_server(server);
+        http::client()->verify_certificates(
+          !UserSettings::instance()->disableCertificateValidation());
+
+        // This starts a chain of `emit`s which ends up doing the
+        // registration. Signals are used rather than normal function
+        // calls so that the dialogs used in UIA work correctly.
+        //
+        // The sequence of events looks something like this:
+        //
+        // doKnownLookup
+        //   v
+        // doVersionsCheck
+        //   v
+        // doRegistration -> loops the UIAHandler until complete
+
+        emit wellKnownLookup();
+
+        emit registering();
+    }
 }
 
 void
 RegisterPage::doWellKnownLookup()
 {
-        http::client()->well_known(
-          [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          if (err->status_code == 404) {
-                                  nhlog::net()->info("Autodiscovery: No .well-known.");
-                                  // Check that the homeserver can be reached
-                                  emit versionsCheck();
-                                  return;
-                          }
-
-                          if (!err->parse_error.empty()) {
-                                  emit serverError(
-                                    tr("Autodiscovery failed. Received malformed response."));
-                                  nhlog::net()->error(
-                                    "Autodiscovery failed. Received malformed response.");
-                                  return;
-                          }
-
-                          emit serverError(tr("Autodiscovery failed. Unknown error when "
-                                              "requesting .well-known."));
-                          nhlog::net()->error("Autodiscovery failed. Unknown error when "
-                                              "requesting .well-known. {} {}",
-                                              err->status_code,
-                                              err->error_code);
-                          return;
-                  }
-
-                  nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
-                  http::client()->set_server(res.homeserver.base_url);
+    http::client()->well_known(
+      [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
+          if (err) {
+              if (err->status_code == 404) {
+                  nhlog::net()->info("Autodiscovery: No .well-known.");
                   // Check that the homeserver can be reached
                   emit versionsCheck();
-          });
+                  return;
+              }
+
+              if (!err->parse_error.empty()) {
+                  emit serverError(tr("Autodiscovery failed. Received malformed response."));
+                  nhlog::net()->error("Autodiscovery failed. Received malformed response.");
+                  return;
+              }
+
+              emit serverError(tr("Autodiscovery failed. Unknown error when "
+                                  "requesting .well-known."));
+              nhlog::net()->error("Autodiscovery failed. Unknown error when "
+                                  "requesting .well-known. {} {}",
+                                  err->status_code,
+                                  err->error_code);
+              return;
+          }
+
+          nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
+          http::client()->set_server(res.homeserver.base_url);
+          // Check that the homeserver can be reached
+          emit versionsCheck();
+      });
 }
 
 void
 RegisterPage::doVersionsCheck()
 {
-        // Make a request to /_matrix/client/versions to check the address
-        // given is a Matrix homeserver.
-        http::client()->versions(
-          [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
-                  if (err) {
-                          if (err->status_code == 404) {
-                                  emit serverError(
-                                    tr("The required endpoints were not found. Possibly "
-                                       "not a Matrix server."));
-                                  return;
-                          }
-
-                          if (!err->parse_error.empty()) {
-                                  emit serverError(
-                                    tr("Received malformed response. Make sure the homeserver "
-                                       "domain is valid."));
-                                  return;
-                          }
-
-                          emit serverError(tr("An unknown error occured. Make sure the "
-                                              "homeserver domain is valid."));
-                          return;
-                  }
-
-                  // Attempt registration without an `auth` dict
-                  emit registration();
-          });
-}
+    // Make a request to /_matrix/client/versions to check the address
+    // given is a Matrix homeserver.
+    http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+        if (err) {
+            if (err->status_code == 404) {
+                emit serverError(tr("The required endpoints were not found. Possibly "
+                                    "not a Matrix server."));
+                return;
+            }
 
-void
-RegisterPage::doRegistration()
-{
-        // These inputs should still be alright, but check just in case
-        if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
-                auto username = username_input_->text().toStdString();
-                auto password = password_input_->text().toStdString();
-                http::client()->registration(username, password, registrationCb());
+            if (!err->parse_error.empty()) {
+                emit serverError(tr("Received malformed response. Make sure the homeserver "
+                                    "domain is valid."));
+                return;
+            }
+
+            emit serverError(tr("An unknown error occured. Make sure the "
+                                "homeserver domain is valid."));
+            return;
         }
+
+        // Attempt registration without an `auth` dict
+        emit registration();
+    });
 }
 
 void
-RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth)
+RegisterPage::doRegistration()
 {
-        // These inputs should still be alright, but check just in case
-        if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
-                auto username = username_input_->text().toStdString();
-                auto password = password_input_->text().toStdString();
-                http::client()->registration(username, password, auth, registrationCb());
-        }
+    // These inputs should still be alright, but check just in case
+    if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
+        auto username = username_input_->text().toStdString();
+        auto password = password_input_->text().toStdString();
+        connect(UIA::instance(), &UIA::error, this, [this](QString msg) {
+            showError(msg);
+            disconnect(UIA::instance(), &UIA::error, this, nullptr);
+        });
+        http::client()->registration(
+          username, password, ::UIA::instance()->genericHandler("Registration"), registrationCb());
+    }
 }
 
 mtx::http::Callback<mtx::responses::Register>
 RegisterPage::registrationCb()
 {
-        // Return a function to be used as the callback when an attempt at
-        // registration is made.
-        return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
-                if (!err) {
-                        http::client()->set_user(res.user_id);
-                        http::client()->set_access_token(res.access_token);
-                        emit registerOk();
-                        return;
-                }
-
-                // The server requires registration flows.
-                if (err->status_code == 401) {
-                        if (err->matrix_error.unauthorized.flows.empty()) {
-                                nhlog::net()->warn("failed to retrieve registration flows: "
-                                                   "status_code({}), matrix_error({}) ",
-                                                   static_cast<int>(err->status_code),
-                                                   err->matrix_error.error);
-                                showError(QString::fromStdString(err->matrix_error.error));
-                                return;
-                        }
-
-                        // Attempt to complete a UIA stage
-                        emit UIA(err->matrix_error.unauthorized);
-                        return;
-                }
-
-                nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
-                                    static_cast<int>(err->status_code),
-                                    err->matrix_error.error);
+    // Return a function to be used as the callback when an attempt at
+    // registration is made.
+    return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
+        if (!err) {
+            http::client()->set_user(res.user_id);
+            http::client()->set_access_token(res.access_token);
+            emit registerOk();
+            disconnect(UIA::instance(), &UIA::error, this, nullptr);
+            return;
+        }
 
+        // The server requires registration flows.
+        if (err->status_code == 401) {
+            if (err->matrix_error.unauthorized.flows.empty()) {
+                nhlog::net()->warn("failed to retrieve registration flows: "
+                                   "status_code({}), matrix_error({}) ",
+                                   static_cast<int>(err->status_code),
+                                   err->matrix_error.error);
                 showError(QString::fromStdString(err->matrix_error.error));
-        };
-}
-
-void
-RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
-{
-        auto completed_stages = unauthorized.completed;
-        auto flows            = unauthorized.flows;
-        auto session =
-          unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session;
-
-        nhlog::ui()->info("Completed stages: {}", completed_stages.size());
-
-        if (!completed_stages.empty()) {
-                // Get rid of all flows which don't start with the sequence of
-                // stages that have already been completed.
-                flows.erase(
-                  std::remove_if(flows.begin(),
-                                 flows.end(),
-                                 [completed_stages](auto flow) {
-                                         if (completed_stages.size() > flow.stages.size())
-                                                 return true;
-                                         for (size_t f = 0; f < completed_stages.size(); f++)
-                                                 if (completed_stages[f] != flow.stages[f])
-                                                         return true;
-                                         return false;
-                                 }),
-                  flows.end());
+            }
+            return;
         }
 
-        if (flows.empty()) {
-                nhlog::ui()->error("No available registration flows!");
-                showError(tr("No supported registration flows!"));
-                return;
-        }
+        nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
+                            static_cast<int>(err->status_code),
+                            err->matrix_error.error);
 
-        auto current_stage = flows.front().stages.at(completed_stages.size());
-
-        if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
-                auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this);
-
-                connect(captchaDialog,
-                        &dialogs::ReCaptcha::confirmation,
-                        this,
-                        [this, session, captchaDialog]() {
-                                captchaDialog->close();
-                                captchaDialog->deleteLater();
-                                doRegistrationWithAuth(mtx::user_interactive::Auth{
-                                  session, mtx::user_interactive::auth::Fallback{}});
-                        });
-
-                connect(
-                  captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred);
-
-                QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); });
-
-        } else if (current_stage == mtx::user_interactive::auth_types::dummy) {
-                doRegistrationWithAuth(
-                  mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}});
-
-        } else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
-                bool ok;
-                QString token =
-                  QInputDialog::getText(this,
-                                        tr("Registration token"),
-                                        tr("Please enter a valid registration token."),
-                                        QLineEdit::Normal,
-                                        QString(),
-                                        &ok);
-
-                if (ok) {
-                        emit registrationWithAuth(mtx::user_interactive::Auth{
-                          session,
-                          mtx::user_interactive::auth::RegistrationToken{token.toStdString()}});
-                } else {
-                        emit errorOccurred();
-                }
-        } else {
-                // use fallback
-                auto dialog = new dialogs::FallbackAuth(
-                  QString::fromStdString(current_stage), QString::fromStdString(session), this);
-
-                connect(
-                  dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() {
-                          dialog->close();
-                          dialog->deleteLater();
-                          emit registrationWithAuth(mtx::user_interactive::Auth{
-                            session, mtx::user_interactive::auth::Fallback{}});
-                  });
-
-                connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred);
-
-                dialog->show();
-        }
+        showError(QString::fromStdString(err->matrix_error.error));
+    };
 }
 
 void
 RegisterPage::paintEvent(QPaintEvent *)
 {
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+    QStyleOption opt;
+    opt.init(this);
+    QPainter p(this);
+    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
 }
diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index 42ea00cba08f0675ed032e7b57d1c2a85f8a18ee..0d7da9ad654bc7a6291680950aac314aa4e650de 100644
--- a/src/RegisterPage.h
+++ b/src/RegisterPage.h
@@ -21,76 +21,72 @@ class QHBoxLayout;
 
 class RegisterPage : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        RegisterPage(QWidget *parent = nullptr);
+    RegisterPage(QWidget *parent = nullptr);
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 signals:
-        void backButtonClicked();
-        void errorOccurred();
+    void backButtonClicked();
+    void errorOccurred();
 
-        //! Used to trigger the corresponding slot outside of the main thread.
-        void serverError(const QString &err);
+    //! Used to trigger the corresponding slot outside of the main thread.
+    void serverError(const QString &err);
 
-        void wellKnownLookup();
-        void versionsCheck();
-        void registration();
-        void UIA(const mtx::user_interactive::Unauthorized &unauthorized);
-        void registrationWithAuth(const mtx::user_interactive::Auth &auth);
+    void wellKnownLookup();
+    void versionsCheck();
+    void registration();
 
-        void registering();
-        void registerOk();
+    void registering();
+    void registerOk();
 
 private slots:
-        void onBackButtonClicked();
-        void onRegisterButtonClicked();
-
-        // function for showing different errors
-        void showError(const QString &msg);
-        void showError(QLabel *label, const QString &msg);
-
-        bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
-        bool checkUsername();
-        bool checkPassword();
-        bool checkPasswordConfirmation();
-        bool checkServer();
-
-        void doWellKnownLookup();
-        void doVersionsCheck();
-        void doRegistration();
-        void doUIA(const mtx::user_interactive::Unauthorized &unauthorized);
-        void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth);
-        mtx::http::Callback<mtx::responses::Register> registrationCb();
+    void onBackButtonClicked();
+    void onRegisterButtonClicked();
+
+    // function for showing different errors
+    void showError(const QString &msg);
+    void showError(QLabel *label, const QString &msg);
+
+    bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
+    bool checkUsername();
+    bool checkPassword();
+    bool checkPasswordConfirmation();
+    bool checkServer();
+
+    void doWellKnownLookup();
+    void doVersionsCheck();
+    void doRegistration();
+    mtx::http::Callback<mtx::responses::Register> registrationCb();
 
 private:
-        QVBoxLayout *top_layout_;
-
-        QHBoxLayout *back_layout_;
-        QHBoxLayout *logo_layout_;
-        QHBoxLayout *button_layout_;
-
-        QLabel *logo_;
-        QLabel *error_label_;
-        QLabel *error_username_label_;
-        QLabel *error_password_label_;
-        QLabel *error_password_confirmation_label_;
-        QLabel *error_server_label_;
-        QLabel *error_registration_token_label_;
-
-        FlatButton *back_button_;
-        RaisedButton *register_button_;
-
-        QWidget *form_widget_;
-        QHBoxLayout *form_wrapper_;
-        QVBoxLayout *form_layout_;
-
-        TextField *username_input_;
-        TextField *password_input_;
-        TextField *password_confirmation_;
-        TextField *server_input_;
-        TextField *registration_token_input_;
+    QVBoxLayout *top_layout_;
+
+    QHBoxLayout *back_layout_;
+    QHBoxLayout *logo_layout_;
+    QHBoxLayout *button_layout_;
+
+    QLabel *logo_;
+    QLabel *error_label_;
+    QLabel *error_username_label_;
+    QLabel *error_password_label_;
+    QLabel *error_password_confirmation_label_;
+    QLabel *error_server_label_;
+    QLabel *error_registration_token_label_;
+
+    FlatButton *back_button_;
+    RaisedButton *register_button_;
+
+    QWidget *form_widget_;
+    QHBoxLayout *form_wrapper_;
+    QVBoxLayout *form_layout_;
+
+    TextField *username_input_;
+    TextField *password_input_;
+    TextField *password_confirmation_;
+    TextField *server_input_;
+    TextField *registration_token_input_;
 };
diff --git a/src/RoomDirectoryModel.cpp b/src/RoomDirectoryModel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..707571d62190d9e698413681421036143555307b
--- /dev/null
+++ b/src/RoomDirectoryModel.cpp
@@ -0,0 +1,216 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RoomDirectoryModel.h"
+#include "Cache.h"
+#include "ChatPage.h"
+
+#include <algorithm>
+
+RoomDirectoryModel::RoomDirectoryModel(QObject *parent, const std::string &server)
+  : QAbstractListModel(parent)
+  , server_(server)
+{
+    connect(ChatPage::instance(), &ChatPage::newRoom, this, [this](const QString &roomid) {
+        auto roomid_ = roomid.toStdString();
+
+        int i = 0;
+        for (const auto &room : publicRoomsData_) {
+            if (room.room_id == roomid_) {
+                emit dataChanged(index(i), index(i), {Roles::CanJoin});
+                break;
+            }
+            i++;
+        }
+    });
+
+    connect(this,
+            &RoomDirectoryModel::fetchedRoomsBatch,
+            this,
+            &RoomDirectoryModel::displayRooms,
+            Qt::QueuedConnection);
+}
+
+QHash<int, QByteArray>
+RoomDirectoryModel::roleNames() const
+{
+    return {
+      {Roles::Name, "name"},
+      {Roles::Id, "roomid"},
+      {Roles::AvatarUrl, "avatarUrl"},
+      {Roles::Topic, "topic"},
+      {Roles::MemberCount, "numMembers"},
+      {Roles::Previewable, "canPreview"},
+      {Roles::CanJoin, "canJoin"},
+    };
+}
+
+void
+RoomDirectoryModel::resetDisplayedData()
+{
+    beginResetModel();
+
+    prevBatch_    = "";
+    nextBatch_    = "";
+    canFetchMore_ = true;
+
+    publicRoomsData_.clear();
+
+    endResetModel();
+}
+
+void
+RoomDirectoryModel::setMatrixServer(const QString &s)
+{
+    server_ = s.toStdString();
+
+    nhlog::ui()->debug("Received matrix server: {}", server_);
+
+    resetDisplayedData();
+}
+
+void
+RoomDirectoryModel::setSearchTerm(const QString &f)
+{
+    userSearchString_ = f.toStdString();
+
+    nhlog::ui()->debug("Received user query: {}", userSearchString_);
+
+    resetDisplayedData();
+}
+
+bool
+RoomDirectoryModel::canJoinRoom(const QString &room) const
+{
+    return !room.isEmpty() && cache::getRoomInfo({room.toStdString()}).empty();
+}
+
+std::vector<std::string>
+RoomDirectoryModel::getViasForRoom(const std::vector<std::string> &aliases)
+{
+    std::vector<std::string> vias;
+
+    vias.reserve(aliases.size());
+
+    std::transform(aliases.begin(), aliases.end(), std::back_inserter(vias), [](const auto &alias) {
+        return alias.substr(alias.find(":") + 1);
+    });
+
+    // When joining a room hosted on a homeserver other than the one the
+    // account has been registered on, the room's server has to be explicitly
+    // specified in the "server_name=..." URL parameter of the Matrix Join Room
+    // request. For more details consult the specs:
+    // https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-join-roomidoralias
+    if (!server_.empty()) {
+        vias.push_back(server_);
+    }
+
+    return vias;
+}
+
+void
+RoomDirectoryModel::joinRoom(const int &index)
+{
+    if (index >= 0 && static_cast<size_t>(index) < publicRoomsData_.size()) {
+        const auto &chunk = publicRoomsData_[index];
+        nhlog::ui()->debug("'Joining room {}", chunk.room_id);
+        ChatPage::instance()->joinRoomVia(chunk.room_id, getViasForRoom(chunk.aliases));
+    }
+}
+
+QVariant
+RoomDirectoryModel::data(const QModelIndex &index, int role) const
+{
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        const auto &room_chunk = publicRoomsData_[index.row()];
+        switch (role) {
+        case Roles::Name:
+            return QString::fromStdString(room_chunk.name);
+        case Roles::Id:
+            return QString::fromStdString(room_chunk.room_id);
+        case Roles::AvatarUrl:
+            return QString::fromStdString(room_chunk.avatar_url);
+        case Roles::Topic:
+            return QString::fromStdString(room_chunk.topic);
+        case Roles::MemberCount:
+            return QVariant::fromValue(room_chunk.num_joined_members);
+        case Roles::Previewable:
+            return QVariant::fromValue(room_chunk.world_readable);
+        case Roles::CanJoin:
+            return canJoinRoom(QString::fromStdString(room_chunk.room_id));
+        }
+    }
+    return {};
+}
+
+void
+RoomDirectoryModel::fetchMore(const QModelIndex &)
+{
+    if (!canFetchMore_)
+        return;
+
+    nhlog::net()->debug("Fetching more rooms from mtxclient...");
+
+    mtx::requests::PublicRooms req;
+    req.limit                      = limit_;
+    req.since                      = prevBatch_;
+    req.filter.generic_search_term = userSearchString_;
+    // req.third_party_instance_id = third_party_instance_id;
+    auto requested_server = server_;
+
+    reachedEndOfPagination_ = false;
+    emit reachedEndOfPaginationChanged();
+
+    loadingMoreRooms_ = true;
+    emit loadingMoreRoomsChanged();
+
+    http::client()->post_public_rooms(
+      req,
+      [requested_server, this, req](const mtx::responses::PublicRooms &res,
+                                    mtx::http::RequestErr err) {
+          loadingMoreRooms_ = false;
+          emit loadingMoreRoomsChanged();
+
+          if (err) {
+              nhlog::net()->error("Failed to retrieve rooms from mtxclient - {} - {} - {}",
+                                  mtx::errors::to_string(err->matrix_error.errcode),
+                                  err->matrix_error.error,
+                                  err->parse_error);
+          } else if (req.filter.generic_search_term == this->userSearchString_ &&
+                     req.since == this->prevBatch_ && requested_server == this->server_) {
+              nhlog::net()->debug("signalling chunk to GUI thread");
+              emit fetchedRoomsBatch(res.chunk, res.next_batch);
+          }
+      },
+      requested_server);
+}
+
+void
+RoomDirectoryModel::displayRooms(std::vector<mtx::responses::PublicRoomsChunk> fetched_rooms,
+                                 const std::string &next_batch)
+{
+    nhlog::net()->debug("Prev batch: {} | Next batch: {}", prevBatch_, next_batch);
+
+    if (fetched_rooms.empty()) {
+        nhlog::net()->error("mtxclient helper thread yielded empty chunk!");
+        return;
+    }
+
+    beginInsertRows(QModelIndex(),
+                    static_cast<int>(publicRoomsData_.size()),
+                    static_cast<int>(publicRoomsData_.size() + fetched_rooms.size()) - 1);
+    this->publicRoomsData_.insert(
+      this->publicRoomsData_.end(), fetched_rooms.begin(), fetched_rooms.end());
+    endInsertRows();
+
+    if (next_batch.empty()) {
+        canFetchMore_           = false;
+        reachedEndOfPagination_ = true;
+        emit reachedEndOfPaginationChanged();
+    }
+
+    prevBatch_ = next_batch;
+
+    nhlog::ui()->debug("Finished loading rooms");
+}
diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h
new file mode 100644
index 0000000000000000000000000000000000000000..4699474b7cde5754795cdb6bd30260740e65539a
--- /dev/null
+++ b/src/RoomDirectoryModel.h
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QHash>
+#include <QString>
+#include <string>
+#include <vector>
+
+#include "MatrixClient.h"
+#include <mtx/responses/public_rooms.hpp>
+#include <mtxclient/http/errors.hpp>
+
+#include "Logging.h"
+
+namespace mtx::http {
+using RequestErr = const std::optional<mtx::http::ClientError> &;
+}
+namespace mtx::responses {
+struct PublicRooms;
+}
+
+class RoomDirectoryModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+    Q_PROPERTY(bool loadingMoreRooms READ loadingMoreRooms NOTIFY loadingMoreRoomsChanged)
+    Q_PROPERTY(
+      bool reachedEndOfPagination READ reachedEndOfPagination NOTIFY reachedEndOfPaginationChanged)
+
+public:
+    explicit RoomDirectoryModel(QObject *parent = nullptr, const std::string &server = "");
+
+    enum Roles
+    {
+        Name = Qt::UserRole,
+        Id,
+        AvatarUrl,
+        Topic,
+        MemberCount,
+        Previewable,
+        CanJoin,
+    };
+    QHash<int, QByteArray> roleNames() const override;
+
+    QVariant data(const QModelIndex &index, int role) const override;
+
+    inline int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        (void)parent;
+        return static_cast<int>(publicRoomsData_.size());
+    }
+
+    bool canFetchMore(const QModelIndex &) const override { return canFetchMore_; }
+
+    bool loadingMoreRooms() const { return loadingMoreRooms_; }
+
+    bool reachedEndOfPagination() const { return reachedEndOfPagination_; }
+
+    void fetchMore(const QModelIndex &) override;
+
+    Q_INVOKABLE void joinRoom(const int &index = -1);
+
+signals:
+    void fetchedRoomsBatch(std::vector<mtx::responses::PublicRoomsChunk> rooms,
+                           const std::string &next_batch);
+    void loadingMoreRoomsChanged();
+    void reachedEndOfPaginationChanged();
+
+public slots:
+    void setMatrixServer(const QString &s = "");
+    void setSearchTerm(const QString &f);
+
+private slots:
+
+    void displayRooms(std::vector<mtx::responses::PublicRoomsChunk> rooms,
+                      const std::string &next_batch);
+
+private:
+    bool canJoinRoom(const QString &room) const;
+
+    static constexpr size_t limit_ = 50;
+
+    std::string server_;
+    std::string userSearchString_;
+    std::string prevBatch_;
+    std::string nextBatch_;
+    bool canFetchMore_{true};
+    bool loadingMoreRooms_{false};
+    bool reachedEndOfPagination_{false};
+    std::vector<mtx::responses::PublicRoomsChunk> publicRoomsData_;
+
+    std::vector<std::string> getViasForRoom(const std::vector<std::string> &room);
+    void resetDisplayedData();
+};
diff --git a/src/RoomsModel.cpp b/src/RoomsModel.cpp
index 80f13756df57d225d047a855ec7b4829ffc9ddc3..8c05b7bb83c80884c798edba34453bafb8d50d21 100644
--- a/src/RoomsModel.cpp
+++ b/src/RoomsModel.cpp
@@ -14,71 +14,67 @@ RoomsModel::RoomsModel(bool showOnlyRoomWithAliases, QObject *parent)
   : QAbstractListModel(parent)
   , showOnlyRoomWithAliases_(showOnlyRoomWithAliases)
 {
-        std::vector<std::string> rooms_ = cache::joinedRooms();
-        roomInfos                       = cache::getRoomInfo(rooms_);
-        if (!showOnlyRoomWithAliases_) {
-                roomids.reserve(rooms_.size());
-                roomAliases.reserve(rooms_.size());
-        }
+    std::vector<std::string> rooms_ = cache::joinedRooms();
+    roomInfos                       = cache::getRoomInfo(rooms_);
+    if (!showOnlyRoomWithAliases_) {
+        roomids.reserve(rooms_.size());
+        roomAliases.reserve(rooms_.size());
+    }
 
-        for (const auto &r : rooms_) {
-                auto roomAliasesList = cache::client()->getRoomAliases(r);
+    for (const auto &r : rooms_) {
+        auto roomAliasesList = cache::client()->getRoomAliases(r);
 
-                if (showOnlyRoomWithAliases_) {
-                        if (roomAliasesList && !roomAliasesList->alias.empty()) {
-                                roomids.push_back(QString::fromStdString(r));
-                                roomAliases.push_back(
-                                  QString::fromStdString(roomAliasesList->alias));
-                        }
-                } else {
-                        roomids.push_back(QString::fromStdString(r));
-                        roomAliases.push_back(
-                          roomAliasesList ? QString::fromStdString(roomAliasesList->alias) : "");
-                }
+        if (showOnlyRoomWithAliases_) {
+            if (roomAliasesList && !roomAliasesList->alias.empty()) {
+                roomids.push_back(QString::fromStdString(r));
+                roomAliases.push_back(QString::fromStdString(roomAliasesList->alias));
+            }
+        } else {
+            roomids.push_back(QString::fromStdString(r));
+            roomAliases.push_back(roomAliasesList ? QString::fromStdString(roomAliasesList->alias)
+                                                  : "");
         }
+    }
 }
 
 QHash<int, QByteArray>
 RoomsModel::roleNames() const
 {
-        return {{CompletionModel::CompletionRole, "completionRole"},
-                {CompletionModel::SearchRole, "searchRole"},
-                {CompletionModel::SearchRole2, "searchRole2"},
-                {Roles::RoomAlias, "roomAlias"},
-                {Roles::AvatarUrl, "avatarUrl"},
-                {Roles::RoomID, "roomid"},
-                {Roles::RoomName, "roomName"}};
+    return {{CompletionModel::CompletionRole, "completionRole"},
+            {CompletionModel::SearchRole, "searchRole"},
+            {CompletionModel::SearchRole2, "searchRole2"},
+            {Roles::RoomAlias, "roomAlias"},
+            {Roles::AvatarUrl, "avatarUrl"},
+            {Roles::RoomID, "roomid"},
+            {Roles::RoomName, "roomName"}};
 }
 
 QVariant
 RoomsModel::data(const QModelIndex &index, int role) const
 {
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                switch (role) {
-                case CompletionModel::CompletionRole: {
-                        if (UserSettings::instance()->markdown()) {
-                                QString percentEncoding =
-                                  QUrl::toPercentEncoding(roomAliases[index.row()]);
-                                return QString("[%1](https://matrix.to/#/%2)")
-                                  .arg(roomAliases[index.row()], percentEncoding);
-                        } else {
-                                return roomAliases[index.row()];
-                        }
-                }
-                case CompletionModel::SearchRole:
-                case Qt::DisplayRole:
-                case Roles::RoomAlias:
-                        return roomAliases[index.row()].toHtmlEscaped();
-                case CompletionModel::SearchRole2:
-                case Roles::RoomName:
-                        return QString::fromStdString(roomInfos.at(roomids[index.row()]).name)
-                          .toHtmlEscaped();
-                case Roles::AvatarUrl:
-                        return QString::fromStdString(
-                          roomInfos.at(roomids[index.row()]).avatar_url);
-                case Roles::RoomID:
-                        return roomids[index.row()];
-                }
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        switch (role) {
+        case CompletionModel::CompletionRole: {
+            if (UserSettings::instance()->markdown()) {
+                QString percentEncoding = QUrl::toPercentEncoding(roomAliases[index.row()]);
+                return QString("[%1](https://matrix.to/#/%2)")
+                  .arg(roomAliases[index.row()], percentEncoding);
+            } else {
+                return roomAliases[index.row()];
+            }
+        }
+        case CompletionModel::SearchRole:
+        case Qt::DisplayRole:
+        case Roles::RoomAlias:
+            return roomAliases[index.row()].toHtmlEscaped();
+        case CompletionModel::SearchRole2:
+        case Roles::RoomName:
+            return QString::fromStdString(roomInfos.at(roomids[index.row()]).name).toHtmlEscaped();
+        case Roles::AvatarUrl:
+            return QString::fromStdString(roomInfos.at(roomids[index.row()]).avatar_url);
+        case Roles::RoomID:
+            return roomids[index.row()].toHtmlEscaped();
         }
-        return {};
+    }
+    return {};
 }
diff --git a/src/RoomsModel.h b/src/RoomsModel.h
index 255f207cdeabd31985965cd3e404ce1dc076983d..b6e2997483c02602652efdbcdb260922d0c395fe 100644
--- a/src/RoomsModel.h
+++ b/src/RoomsModel.h
@@ -12,26 +12,26 @@
 class RoomsModel : public QAbstractListModel
 {
 public:
-        enum Roles
-        {
-                AvatarUrl = Qt::UserRole,
-                RoomAlias,
-                RoomID,
-                RoomName,
-        };
+    enum Roles
+    {
+        AvatarUrl = Qt::UserRole,
+        RoomAlias,
+        RoomID,
+        RoomName,
+    };
 
-        RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr);
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override
-        {
-                (void)parent;
-                return (int)roomids.size();
-        }
-        QVariant data(const QModelIndex &index, int role) const override;
+    RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr);
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        (void)parent;
+        return (int)roomids.size();
+    }
+    QVariant data(const QModelIndex &index, int role) const override;
 
 private:
-        std::vector<QString> roomids;
-        std::vector<QString> roomAliases;
-        std::map<QString, RoomInfo> roomInfos;
-        bool showOnlyRoomWithAliases_;
+    std::vector<QString> roomids;
+    std::vector<QString> roomAliases;
+    std::map<QString, RoomInfo> roomInfos;
+    bool showOnlyRoomWithAliases_;
 };
diff --git a/src/SSOHandler.cpp b/src/SSOHandler.cpp
index 8fd0828cdaa24abe052dd3be12a36253697bedd1..a6f7ba11467a029eea0a49dddcb2613a69cdd2a5 100644
--- a/src/SSOHandler.cpp
+++ b/src/SSOHandler.cpp
@@ -12,46 +12,46 @@
 
 SSOHandler::SSOHandler(QObject *)
 {
-        QTimer::singleShot(120000, this, &SSOHandler::ssoFailed);
-
-        using namespace httplib;
-
-        svr.set_logger([](const Request &req, const Response &res) {
-                nhlog::net()->info("req: {}, res: {}", req.path, res.status);
-        });
-
-        svr.Get("/sso", [this](const Request &req, Response &res) {
-                if (req.has_param("loginToken")) {
-                        auto val = req.get_param_value("loginToken");
-                        res.set_content("SSO success", "text/plain");
-                        emit ssoSuccess(val);
-                } else {
-                        res.set_content("Missing loginToken for SSO login!", "text/plain");
-                        emit ssoFailed();
-                }
-        });
-
-        std::thread t([this]() {
-                this->port = svr.bind_to_any_port("localhost");
-                svr.listen_after_bind();
-        });
-        t.detach();
-
-        while (!svr.is_running()) {
-                std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    QTimer::singleShot(120000, this, &SSOHandler::ssoFailed);
+
+    using namespace httplib;
+
+    svr.set_logger([](const Request &req, const Response &res) {
+        nhlog::net()->info("req: {}, res: {}", req.path, res.status);
+    });
+
+    svr.Get("/sso", [this](const Request &req, Response &res) {
+        if (req.has_param("loginToken")) {
+            auto val = req.get_param_value("loginToken");
+            res.set_content("SSO success", "text/plain");
+            emit ssoSuccess(val);
+        } else {
+            res.set_content("Missing loginToken for SSO login!", "text/plain");
+            emit ssoFailed();
         }
+    });
+
+    std::thread t([this]() {
+        this->port = svr.bind_to_any_port("localhost");
+        svr.listen_after_bind();
+    });
+    t.detach();
+
+    while (!svr.is_running()) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
 }
 
 SSOHandler::~SSOHandler()
 {
-        svr.stop();
-        while (svr.is_running()) {
-                std::this_thread::sleep_for(std::chrono::milliseconds(1));
-        }
+    svr.stop();
+    while (svr.is_running()) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
 }
 
 std::string
 SSOHandler::url() const
 {
-        return "http://localhost:" + std::to_string(port) + "/sso";
+    return "http://localhost:" + std::to_string(port) + "/sso";
 }
diff --git a/src/SSOHandler.h b/src/SSOHandler.h
index bd0d424dd68ae58898509db2340c9c0d19470ba9..ab652a067747041cbac8e547520c7e3db365b13b 100644
--- a/src/SSOHandler.h
+++ b/src/SSOHandler.h
@@ -9,20 +9,20 @@
 
 class SSOHandler : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        SSOHandler(QObject *parent = nullptr);
+    SSOHandler(QObject *parent = nullptr);
 
-        ~SSOHandler();
+    ~SSOHandler();
 
-        std::string url() const;
+    std::string url() const;
 
 signals:
-        void ssoSuccess(std::string token);
-        void ssoFailed();
+    void ssoSuccess(std::string token);
+    void ssoFailed();
 
 private:
-        httplib::Server svr;
-        int port = 0;
+    httplib::Server svr;
+    int port = 0;
 };
diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp
index 7bf556173f30d65778f1dda037df22a0b33becbb..978a04804c1ecbd40dc2ad034c3db156f42531a8 100644
--- a/src/SingleImagePackModel.cpp
+++ b/src/SingleImagePackModel.cpp
@@ -24,327 +24,336 @@ SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
   , old_statekey_(statekey_)
   , pack(std::move(pack_.pack))
 {
-        [[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
+    [[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
 
-        if (!pack.pack)
-                pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
+    if (!pack.pack)
+        pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
 
-        for (const auto &e : pack.images)
-                shortcodes.push_back(e.first);
+    for (const auto &e : pack.images)
+        shortcodes.push_back(e.first);
 
-        connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
+    connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
 }
 
 int
 SingleImagePackModel::rowCount(const QModelIndex &) const
 {
-        return (int)shortcodes.size();
+    return (int)shortcodes.size();
 }
 
 QHash<int, QByteArray>
 SingleImagePackModel::roleNames() const
 {
-        return {
-          {Roles::Url, "url"},
-          {Roles::ShortCode, "shortCode"},
-          {Roles::Body, "body"},
-          {Roles::IsEmote, "isEmote"},
-          {Roles::IsSticker, "isSticker"},
-        };
+    return {
+      {Roles::Url, "url"},
+      {Roles::ShortCode, "shortCode"},
+      {Roles::Body, "body"},
+      {Roles::IsEmote, "isEmote"},
+      {Roles::IsSticker, "isSticker"},
+    };
 }
 
 QVariant
 SingleImagePackModel::data(const QModelIndex &index, int role) const
 {
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                const auto &img = pack.images.at(shortcodes.at(index.row()));
-                switch (role) {
-                case Url:
-                        return QString::fromStdString(img.url);
-                case ShortCode:
-                        return QString::fromStdString(shortcodes.at(index.row()));
-                case Body:
-                        return QString::fromStdString(img.body);
-                case IsEmote:
-                        return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
-                case IsSticker:
-                        return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
-                default:
-                        return {};
-                }
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        const auto &img = pack.images.at(shortcodes.at(index.row()));
+        switch (role) {
+        case Url:
+            return QString::fromStdString(img.url);
+        case ShortCode:
+            return QString::fromStdString(shortcodes.at(index.row()));
+        case Body:
+            return QString::fromStdString(img.body);
+        case IsEmote:
+            return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
+        case IsSticker:
+            return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
+        default:
+            return {};
         }
-        return {};
+    }
+    return {};
 }
 
 bool
 SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
 {
-        using mtx::events::msc2545::PackUsage;
-
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                auto &img = pack.images.at(shortcodes.at(index.row()));
-                switch (role) {
-                case ShortCode: {
-                        auto newCode = value.toString().toStdString();
-
-                        // otherwise we delete this by accident
-                        if (pack.images.count(newCode))
-                                return false;
-
-                        auto tmp     = img;
-                        auto oldCode = shortcodes.at(index.row());
-                        pack.images.erase(oldCode);
-                        shortcodes[index.row()] = newCode;
-                        pack.images.insert({newCode, tmp});
-
-                        emit dataChanged(
-                          this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
-                        return true;
-                }
-                case Body:
-                        img.body = value.toString().toStdString();
-                        emit dataChanged(
-                          this->index(index.row()), this->index(index.row()), {Roles::Body});
-                        return true;
-                case IsEmote: {
-                        bool isEmote = value.toBool();
-                        bool isSticker =
-                          img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
-
-                        img.usage.set(PackUsage::Emoji, isEmote);
-                        img.usage.set(PackUsage::Sticker, isSticker);
-
-                        if (img.usage == pack.pack->usage)
-                                img.usage.reset();
-
-                        emit dataChanged(
-                          this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
-
-                        return true;
-                }
-                case IsSticker: {
-                        bool isEmote =
-                          img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
-                        bool isSticker = value.toBool();
-
-                        img.usage.set(PackUsage::Emoji, isEmote);
-                        img.usage.set(PackUsage::Sticker, isSticker);
-
-                        if (img.usage == pack.pack->usage)
-                                img.usage.reset();
-
-                        emit dataChanged(
-                          this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
-
-                        return true;
-                }
-                }
+    using mtx::events::msc2545::PackUsage;
+
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        auto &img = pack.images.at(shortcodes.at(index.row()));
+        switch (role) {
+        case ShortCode: {
+            auto newCode = value.toString().toStdString();
+
+            // otherwise we delete this by accident
+            if (pack.images.count(newCode))
+                return false;
+
+            auto tmp     = img;
+            auto oldCode = shortcodes.at(index.row());
+            pack.images.erase(oldCode);
+            shortcodes[index.row()] = newCode;
+            pack.images.insert({newCode, tmp});
+
+            emit dataChanged(
+              this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
+            return true;
         }
-        return false;
+        case Body:
+            img.body = value.toString().toStdString();
+            emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::Body});
+            return true;
+        case IsEmote: {
+            bool isEmote   = value.toBool();
+            bool isSticker = img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
+
+            img.usage.set(PackUsage::Emoji, isEmote);
+            img.usage.set(PackUsage::Sticker, isSticker);
+
+            if (img.usage == pack.pack->usage)
+                img.usage.reset();
+
+            emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
+
+            return true;
+        }
+        case IsSticker: {
+            bool isEmote   = img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
+            bool isSticker = value.toBool();
+
+            img.usage.set(PackUsage::Emoji, isEmote);
+            img.usage.set(PackUsage::Sticker, isSticker);
+
+            if (img.usage == pack.pack->usage)
+                img.usage.reset();
+
+            emit dataChanged(
+              this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
+
+            return true;
+        }
+        }
+    }
+    return false;
 }
 
 bool
 SingleImagePackModel::isGloballyEnabled() const
 {
-        if (auto roomPacks =
-              cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
-                if (auto tmp = std::get_if<
-                      mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
-                      &*roomPacks)) {
-                        if (tmp->content.rooms.count(roomid_) &&
-                            tmp->content.rooms.at(roomid_).count(statekey_))
-                                return true;
-                }
+    if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
+        if (auto tmp =
+              std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
+                &*roomPacks)) {
+            if (tmp->content.rooms.count(roomid_) &&
+                tmp->content.rooms.at(roomid_).count(statekey_))
+                return true;
         }
-        return false;
+    }
+    return false;
 }
 void
 SingleImagePackModel::setGloballyEnabled(bool enabled)
 {
-        mtx::events::msc2545::ImagePackRooms content{};
-        if (auto roomPacks =
-              cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
-                if (auto tmp = std::get_if<
-                      mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
-                      &*roomPacks)) {
-                        content = tmp->content;
-                }
+    mtx::events::msc2545::ImagePackRooms content{};
+    if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
+        if (auto tmp =
+              std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
+                &*roomPacks)) {
+            content = tmp->content;
         }
+    }
 
-        if (enabled)
-                content.rooms[roomid_][statekey_] = {};
-        else
-                content.rooms[roomid_].erase(statekey_);
+    if (enabled)
+        content.rooms[roomid_][statekey_] = {};
+    else
+        content.rooms[roomid_].erase(statekey_);
 
-        http::client()->put_account_data(content, [](mtx::http::RequestErr) {
-                // emit this->globallyEnabledChanged();
-        });
+    http::client()->put_account_data(content, [](mtx::http::RequestErr) {
+        // emit this->globallyEnabledChanged();
+    });
 }
 
 bool
 SingleImagePackModel::canEdit() const
 {
-        if (roomid_.empty())
-                return true;
-        else
-                return Permissions(QString::fromStdString(roomid_))
-                  .canChange(qml_mtx_events::ImagePackInRoom);
+    if (roomid_.empty())
+        return true;
+    else
+        return Permissions(QString::fromStdString(roomid_))
+          .canChange(qml_mtx_events::ImagePackInRoom);
 }
 
 void
 SingleImagePackModel::setPackname(QString val)
 {
-        auto val_ = val.toStdString();
-        if (val_ != this->pack.pack->display_name) {
-                this->pack.pack->display_name = val_;
-                emit packnameChanged();
-        }
+    auto val_ = val.toStdString();
+    if (val_ != this->pack.pack->display_name) {
+        this->pack.pack->display_name = val_;
+        emit packnameChanged();
+    }
 }
 
 void
 SingleImagePackModel::setAttribution(QString val)
 {
-        auto val_ = val.toStdString();
-        if (val_ != this->pack.pack->attribution) {
-                this->pack.pack->attribution = val_;
-                emit attributionChanged();
-        }
+    auto val_ = val.toStdString();
+    if (val_ != this->pack.pack->attribution) {
+        this->pack.pack->attribution = val_;
+        emit attributionChanged();
+    }
 }
 
 void
 SingleImagePackModel::setAvatarUrl(QString val)
 {
-        auto val_ = val.toStdString();
-        if (val_ != this->pack.pack->avatar_url) {
-                this->pack.pack->avatar_url = val_;
-                emit avatarUrlChanged();
-        }
+    auto val_ = val.toStdString();
+    if (val_ != this->pack.pack->avatar_url) {
+        this->pack.pack->avatar_url = val_;
+        emit avatarUrlChanged();
+    }
 }
 
 void
 SingleImagePackModel::setStatekey(QString val)
 {
-        auto val_ = val.toStdString();
-        if (val_ != statekey_) {
-                statekey_ = val_;
-                emit statekeyChanged();
-        }
+    auto val_ = val.toStdString();
+    if (val_ != statekey_) {
+        statekey_ = val_;
+        emit statekeyChanged();
+    }
 }
 
 void
 SingleImagePackModel::setIsStickerPack(bool val)
 {
-        using mtx::events::msc2545::PackUsage;
-        if (val != pack.pack->is_sticker()) {
-                pack.pack->usage.set(PackUsage::Sticker, val);
-                emit isStickerPackChanged();
-        }
+    using mtx::events::msc2545::PackUsage;
+    if (val != pack.pack->is_sticker()) {
+        pack.pack->usage.set(PackUsage::Sticker, val);
+        emit isStickerPackChanged();
+    }
 }
 
 void
 SingleImagePackModel::setIsEmotePack(bool val)
 {
-        using mtx::events::msc2545::PackUsage;
-        if (val != pack.pack->is_emoji()) {
-                pack.pack->usage.set(PackUsage::Emoji, val);
-                emit isEmotePackChanged();
-        }
+    using mtx::events::msc2545::PackUsage;
+    if (val != pack.pack->is_emoji()) {
+        pack.pack->usage.set(PackUsage::Emoji, val);
+        emit isEmotePackChanged();
+    }
 }
 
 void
 SingleImagePackModel::save()
 {
-        if (roomid_.empty()) {
-                http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
-                        if (e)
-                                ChatPage::instance()->showNotification(
-                                  tr("Failed to update image pack: {}")
-                                    .arg(QString::fromStdString(e->matrix_error.error)));
-                });
-        } else {
-                if (old_statekey_ != statekey_) {
-                        http::client()->send_state_event(
-                          roomid_,
-                          to_string(mtx::events::EventType::ImagePackInRoom),
-                          old_statekey_,
-                          nlohmann::json::object(),
-                          [](const mtx::responses::EventId &, mtx::http::RequestErr e) {
-                                  if (e)
-                                          ChatPage::instance()->showNotification(
-                                            tr("Failed to delete old image pack: {}")
-                                              .arg(QString::fromStdString(e->matrix_error.error)));
-                          });
-                }
-
-                http::client()->send_state_event(
-                  roomid_,
-                  statekey_,
-                  pack,
-                  [this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
-                          if (e)
-                                  ChatPage::instance()->showNotification(
-                                    tr("Failed to update image pack: {}")
-                                      .arg(QString::fromStdString(e->matrix_error.error)));
-
-                          nhlog::net()->info("Uploaded image pack: {}", statekey_);
-                  });
+    if (roomid_.empty()) {
+        http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
+            if (e)
+                ChatPage::instance()->showNotification(
+                  tr("Failed to update image pack: %1")
+                    .arg(QString::fromStdString(e->matrix_error.error)));
+        });
+    } else {
+        if (old_statekey_ != statekey_) {
+            http::client()->send_state_event(
+              roomid_,
+              to_string(mtx::events::EventType::ImagePackInRoom),
+              old_statekey_,
+              nlohmann::json::object(),
+              [](const mtx::responses::EventId &, mtx::http::RequestErr e) {
+                  if (e)
+                      ChatPage::instance()->showNotification(
+                        tr("Failed to delete old image pack: %1")
+                          .arg(QString::fromStdString(e->matrix_error.error)));
+              });
         }
+
+        http::client()->send_state_event(
+          roomid_,
+          statekey_,
+          pack,
+          [this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
+              if (e)
+                  ChatPage::instance()->showNotification(
+                    tr("Failed to update image pack: %1")
+                      .arg(QString::fromStdString(e->matrix_error.error)));
+
+              nhlog::net()->info("Uploaded image pack: %1", statekey_);
+          });
+    }
 }
 
 void
 SingleImagePackModel::addStickers(QList<QUrl> files)
 {
-        for (const auto &f : files) {
-                auto file = QFile(f.toLocalFile());
-                if (!file.open(QFile::ReadOnly)) {
-                        ChatPage::instance()->showNotification(
-                          tr("Failed to open image: {}").arg(f.toLocalFile()));
-                        return;
-                }
-
-                auto bytes = file.readAll();
-                auto img   = utils::readImage(bytes);
-
-                mtx::common::ImageInfo info{};
-
-                auto sz = img.size() / 2;
-                if (sz.width() > 512 || sz.height() > 512) {
-                        sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
-                }
-
-                info.h    = sz.height();
-                info.w    = sz.width();
-                info.size = bytes.size();
-
-                auto filename = f.fileName().toStdString();
-                http::client()->upload(
-                  bytes.toStdString(),
-                  QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
-                  filename,
-                  [this, filename, info](const mtx::responses::ContentURI &uri,
-                                         mtx::http::RequestErr e) {
-                          if (e) {
-                                  ChatPage::instance()->showNotification(
-                                    tr("Failed to upload image: {}")
-                                      .arg(QString::fromStdString(e->matrix_error.error)));
-                                  return;
-                          }
-
-                          emit addImage(uri.content_uri, filename, info);
-                  });
+    for (const auto &f : files) {
+        auto file = QFile(f.toLocalFile());
+        if (!file.open(QFile::ReadOnly)) {
+            ChatPage::instance()->showNotification(
+              tr("Failed to open image: %1").arg(f.toLocalFile()));
+            return;
+        }
+
+        auto bytes = file.readAll();
+        auto img   = utils::readImage(bytes);
+
+        mtx::common::ImageInfo info{};
+
+        auto sz = img.size() / 2;
+        if (sz.width() > 512 || sz.height() > 512) {
+            sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
+        } else if (img.height() < 128 && img.width() < 128) {
+            sz = img.size();
         }
+
+        info.h        = sz.height();
+        info.w        = sz.width();
+        info.size     = bytes.size();
+        info.mimetype = QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString();
+
+        auto filename = f.fileName().toStdString();
+        http::client()->upload(
+          bytes.toStdString(),
+          QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
+          filename,
+          [this, filename, info](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) {
+              if (e) {
+                  ChatPage::instance()->showNotification(
+                    tr("Failed to upload image: %1")
+                      .arg(QString::fromStdString(e->matrix_error.error)));
+                  return;
+              }
+
+              emit addImage(uri.content_uri, filename, info);
+          });
+    }
+}
+
+void
+SingleImagePackModel::remove(int idx)
+{
+    if (idx < (int)shortcodes.size() && idx >= 0) {
+        beginRemoveRows(QModelIndex(), idx, idx);
+        auto s = shortcodes.at(idx);
+        shortcodes.erase(shortcodes.begin() + idx);
+        pack.images.erase(s);
+        endRemoveRows();
+    }
 }
+
 void
 SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info)
 {
-        mtx::events::msc2545::PackImage img{};
-        img.url  = uri;
-        img.info = info;
-        beginInsertRows(
-          QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));
+    mtx::events::msc2545::PackImage img{};
+    img.url  = uri;
+    img.info = info;
+    beginInsertRows(
+      QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));
 
-        pack.images[filename] = img;
-        shortcodes.push_back(filename);
+    pack.images[filename] = img;
+    shortcodes.push_back(filename);
 
-        endInsertRows();
+    endInsertRows();
 }
diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h
index cd38b3b6909e2ca9a0da6e78d13b1b0ffb707471..cd8b05475f400e587a08359ea6748b7ec1b96304 100644
--- a/src/SingleImagePackModel.h
+++ b/src/SingleImagePackModel.h
@@ -14,80 +14,78 @@
 
 class SingleImagePackModel : public QAbstractListModel
 {
-        Q_OBJECT
-
-        Q_PROPERTY(QString roomid READ roomid CONSTANT)
-        Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
-        Q_PROPERTY(
-          QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
-        Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
-        Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
-        Q_PROPERTY(
-          bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged)
-        Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged)
-        Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
-                     globallyEnabledChanged)
-        Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
+    Q_OBJECT
+
+    Q_PROPERTY(QString roomid READ roomid CONSTANT)
+    Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
+    Q_PROPERTY(QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
+    Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
+    Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
+    Q_PROPERTY(
+      bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged)
+    Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged)
+    Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
+                 globallyEnabledChanged)
+    Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
 
 public:
-        enum Roles
-        {
-                Url = Qt::UserRole,
-                ShortCode,
-                Body,
-                IsEmote,
-                IsSticker,
-        };
-        Q_ENUM(Roles);
-
-        SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-        QVariant data(const QModelIndex &index, int role) const override;
-        bool setData(const QModelIndex &index,
-                     const QVariant &value,
-                     int role = Qt::EditRole) override;
-
-        QString roomid() const { return QString::fromStdString(roomid_); }
-        QString statekey() const { return QString::fromStdString(statekey_); }
-        QString packname() const { return QString::fromStdString(pack.pack->display_name); }
-        QString attribution() const { return QString::fromStdString(pack.pack->attribution); }
-        QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); }
-        bool isStickerPack() const { return pack.pack->is_sticker(); }
-        bool isEmotePack() const { return pack.pack->is_emoji(); }
-
-        bool isGloballyEnabled() const;
-        bool canEdit() const;
-        void setGloballyEnabled(bool enabled);
-
-        void setPackname(QString val);
-        void setAttribution(QString val);
-        void setAvatarUrl(QString val);
-        void setStatekey(QString val);
-        void setIsStickerPack(bool val);
-        void setIsEmotePack(bool val);
-
-        Q_INVOKABLE void save();
-        Q_INVOKABLE void addStickers(QList<QUrl> files);
+    enum Roles
+    {
+        Url = Qt::UserRole,
+        ShortCode,
+        Body,
+        IsEmote,
+        IsSticker,
+    };
+    Q_ENUM(Roles);
+
+    SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+    QVariant data(const QModelIndex &index, int role) const override;
+    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+
+    QString roomid() const { return QString::fromStdString(roomid_); }
+    QString statekey() const { return QString::fromStdString(statekey_); }
+    QString packname() const { return QString::fromStdString(pack.pack->display_name); }
+    QString attribution() const { return QString::fromStdString(pack.pack->attribution); }
+    QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); }
+    bool isStickerPack() const { return pack.pack->is_sticker(); }
+    bool isEmotePack() const { return pack.pack->is_emoji(); }
+
+    bool isGloballyEnabled() const;
+    bool canEdit() const;
+    void setGloballyEnabled(bool enabled);
+
+    void setPackname(QString val);
+    void setAttribution(QString val);
+    void setAvatarUrl(QString val);
+    void setStatekey(QString val);
+    void setIsStickerPack(bool val);
+    void setIsEmotePack(bool val);
+
+    Q_INVOKABLE void save();
+    Q_INVOKABLE void addStickers(QList<QUrl> files);
+    Q_INVOKABLE void remove(int index);
 
 signals:
-        void globallyEnabledChanged();
-        void statekeyChanged();
-        void attributionChanged();
-        void packnameChanged();
-        void avatarUrlChanged();
-        void isEmotePackChanged();
-        void isStickerPackChanged();
+    void globallyEnabledChanged();
+    void statekeyChanged();
+    void attributionChanged();
+    void packnameChanged();
+    void avatarUrlChanged();
+    void isEmotePackChanged();
+    void isStickerPackChanged();
 
-        void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info);
+    void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info);
 
 private slots:
-        void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info);
+    void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info);
 
 private:
-        std::string roomid_;
-        std::string statekey_, old_statekey_;
+    std::string roomid_;
+    std::string statekey_, old_statekey_;
 
-        mtx::events::msc2545::ImagePack pack;
-        std::vector<std::string> shortcodes;
+    mtx::events::msc2545::ImagePack pack;
+    std::vector<std::string> shortcodes;
 };
diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp
index db0130c84b2579351e5286620c01634a3bbee17f..98a1d242dd5a40e1634102b77db4aa10fac28aa5 100644
--- a/src/TrayIcon.cpp
+++ b/src/TrayIcon.cpp
@@ -19,7 +19,7 @@
 MsgCountComposedIcon::MsgCountComposedIcon(const QString &filename)
   : QIconEngine()
 {
-        icon_ = QIcon(filename);
+    icon_ = QIcon(filename);
 }
 
 void
@@ -28,95 +28,95 @@ MsgCountComposedIcon::paint(QPainter *painter,
                             QIcon::Mode mode,
                             QIcon::State state)
 {
-        painter->setRenderHint(QPainter::TextAntialiasing);
-        painter->setRenderHint(QPainter::SmoothPixmapTransform);
-        painter->setRenderHint(QPainter::Antialiasing);
-
-        icon_.paint(painter, rect, Qt::AlignCenter, mode, state);
-
-        if (msgCount <= 0)
-                return;
-
-        QColor backgroundColor("red");
-        QColor textColor("white");
-
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
-        brush.setColor(backgroundColor);
-
-        QFont f;
-        f.setPointSizeF(8);
-        f.setWeight(QFont::Thin);
-
-        painter->setBrush(brush);
-        painter->setPen(Qt::NoPen);
-        painter->setFont(f);
-
-        QRectF bubble(rect.width() - BubbleDiameter,
-                      rect.height() - BubbleDiameter,
-                      BubbleDiameter,
-                      BubbleDiameter);
-        painter->drawEllipse(bubble);
-        painter->setPen(QPen(textColor));
-        painter->setBrush(Qt::NoBrush);
-        painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount));
+    painter->setRenderHint(QPainter::TextAntialiasing);
+    painter->setRenderHint(QPainter::SmoothPixmapTransform);
+    painter->setRenderHint(QPainter::Antialiasing);
+
+    icon_.paint(painter, rect, Qt::AlignCenter, mode, state);
+
+    if (msgCount <= 0)
+        return;
+
+    QColor backgroundColor("red");
+    QColor textColor("white");
+
+    QBrush brush;
+    brush.setStyle(Qt::SolidPattern);
+    brush.setColor(backgroundColor);
+
+    QFont f;
+    f.setPointSizeF(8);
+    f.setWeight(QFont::Thin);
+
+    painter->setBrush(brush);
+    painter->setPen(Qt::NoPen);
+    painter->setFont(f);
+
+    QRectF bubble(rect.width() - BubbleDiameter,
+                  rect.height() - BubbleDiameter,
+                  BubbleDiameter,
+                  BubbleDiameter);
+    painter->drawEllipse(bubble);
+    painter->setPen(QPen(textColor));
+    painter->setBrush(Qt::NoBrush);
+    painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount));
 }
 
 QIconEngine *
 MsgCountComposedIcon::clone() const
 {
-        return new MsgCountComposedIcon(*this);
+    return new MsgCountComposedIcon(*this);
 }
 
 QList<QSize>
 MsgCountComposedIcon::availableSizes(QIcon::Mode mode, QIcon::State state) const
 {
-        Q_UNUSED(mode);
-        Q_UNUSED(state);
-        QList<QSize> sizes;
-        sizes.append(QSize(24, 24));
-        sizes.append(QSize(32, 32));
-        sizes.append(QSize(48, 48));
-        sizes.append(QSize(64, 64));
-        sizes.append(QSize(128, 128));
-        sizes.append(QSize(256, 256));
-        return sizes;
+    Q_UNUSED(mode);
+    Q_UNUSED(state);
+    QList<QSize> sizes;
+    sizes.append(QSize(24, 24));
+    sizes.append(QSize(32, 32));
+    sizes.append(QSize(48, 48));
+    sizes.append(QSize(64, 64));
+    sizes.append(QSize(128, 128));
+    sizes.append(QSize(256, 256));
+    return sizes;
 }
 
 QPixmap
 MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
 {
-        QImage img(size, QImage::Format_ARGB32);
-        img.fill(qRgba(0, 0, 0, 0));
-        QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion);
-        {
-                QPainter painter(&result);
-                paint(&painter, QRect(QPoint(0, 0), size), mode, state);
-        }
-        return result;
+    QImage img(size, QImage::Format_ARGB32);
+    img.fill(qRgba(0, 0, 0, 0));
+    QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion);
+    {
+        QPainter painter(&result);
+        paint(&painter, QRect(QPoint(0, 0), size), mode, state);
+    }
+    return result;
 }
 
 TrayIcon::TrayIcon(const QString &filename, QWidget *parent)
   : QSystemTrayIcon(parent)
 {
 #if defined(Q_OS_MAC) || defined(Q_OS_WIN)
-        setIcon(QIcon(filename));
+    setIcon(QIcon(filename));
 #else
-        icon_ = new MsgCountComposedIcon(filename);
-        setIcon(QIcon(icon_));
+    icon_ = new MsgCountComposedIcon(filename);
+    setIcon(QIcon(icon_));
 #endif
 
-        QMenu *menu = new QMenu(parent);
-        setContextMenu(menu);
+    QMenu *menu = new QMenu(parent);
+    setContextMenu(menu);
 
-        viewAction_ = new QAction(tr("Show"), this);
-        quitAction_ = new QAction(tr("Quit"), this);
+    viewAction_ = new QAction(tr("Show"), this);
+    quitAction_ = new QAction(tr("Quit"), this);
 
-        connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show()));
-        connect(quitAction_, &QAction::triggered, this, QApplication::quit);
+    connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show()));
+    connect(quitAction_, &QAction::triggered, this, QApplication::quit);
 
-        menu->addAction(viewAction_);
-        menu->addAction(quitAction_);
+    menu->addAction(viewAction_);
+    menu->addAction(quitAction_);
 }
 
 void
@@ -127,25 +127,25 @@ TrayIcon::setUnreadCount(int count)
 // currently, to avoid writing obj-c code, ignore deprecated warnings on the badge functions
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
-        auto labelText = count == 0 ? "" : QString::number(count);
+    auto labelText = count == 0 ? "" : QString::number(count);
 
-        if (labelText == QtMac::badgeLabelText())
-                return;
+    if (labelText == QtMac::badgeLabelText())
+        return;
 
-        QtMac::setBadgeLabelText(labelText);
+    QtMac::setBadgeLabelText(labelText);
 #pragma clang diagnostic pop
 #elif defined(Q_OS_WIN)
 // FIXME: Find a way to use Windows apis for the badge counter (if any).
 #else
-        if (count == icon_->msgCount)
-                return;
+    if (count == icon_->msgCount)
+        return;
 
-        // Custom drawing on Linux.
-        MsgCountComposedIcon *tmp = static_cast<MsgCountComposedIcon *>(icon_->clone());
-        tmp->msgCount             = count;
+    // Custom drawing on Linux.
+    MsgCountComposedIcon *tmp = static_cast<MsgCountComposedIcon *>(icon_->clone());
+    tmp->msgCount             = count;
 
-        setIcon(QIcon(tmp));
+    setIcon(QIcon(tmp));
 
-        icon_ = tmp;
+    icon_ = tmp;
 #endif
 }
diff --git a/src/TrayIcon.h b/src/TrayIcon.h
index 10dfafc5ddeb4e93460b0a3cf03bb3728e7940d0..1ce7fb0bb02dfe0643546f0bd74dbbbf6bc257d5 100644
--- a/src/TrayIcon.h
+++ b/src/TrayIcon.h
@@ -16,33 +16,33 @@ class QPainter;
 class MsgCountComposedIcon : public QIconEngine
 {
 public:
-        MsgCountComposedIcon(const QString &filename);
+    MsgCountComposedIcon(const QString &filename);
 
-        void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override;
-        QIconEngine *clone() const override;
-        QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const override;
-        QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override;
+    void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override;
+    QIconEngine *clone() const override;
+    QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const override;
+    QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override;
 
-        int msgCount = 0;
+    int msgCount = 0;
 
 private:
-        const int BubbleDiameter = 17;
+    const int BubbleDiameter = 17;
 
-        QIcon icon_;
+    QIcon icon_;
 };
 
 class TrayIcon : public QSystemTrayIcon
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        TrayIcon(const QString &filename, QWidget *parent);
+    TrayIcon(const QString &filename, QWidget *parent);
 
 public slots:
-        void setUnreadCount(int count);
+    void setUnreadCount(int count);
 
 private:
-        QAction *viewAction_;
-        QAction *quitAction_;
+    QAction *viewAction_;
+    QAction *quitAction_;
 
-        MsgCountComposedIcon *icon_;
+    MsgCountComposedIcon *icon_;
 };
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index f67c5e2debdf11e46375618394da5fe6cd4d24dc..340709a60e83b84c0bd910fb2f138fda7724b0e0 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -14,7 +14,6 @@
 #include <QLineEdit>
 #include <QMessageBox>
 #include <QPainter>
-#include <QProcessEnvironment>
 #include <QPushButton>
 #include <QResizeEvent>
 #include <QScrollArea>
@@ -26,14 +25,14 @@
 #include <QtQml>
 
 #include "Cache.h"
-#include "CallDevices.h"
 #include "Config.h"
 #include "MatrixClient.h"
-#include "Olm.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "encryption/Olm.h"
 #include "ui/FlatButton.h"
 #include "ui/ToggleButton.h"
+#include "voip/CallDevices.h"
 
 #include "config/nheko.h"
 
@@ -41,1420 +40,1484 @@ QSharedPointer<UserSettings> UserSettings::instance_;
 
 UserSettings::UserSettings()
 {
-        connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() {
-                instance_.clear();
-        });
+    connect(
+      QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); });
 }
 
 QSharedPointer<UserSettings>
 UserSettings::instance()
 {
-        return instance_;
+    return instance_;
 }
 
 void
 UserSettings::initialize(std::optional<QString> profile)
 {
-        instance_.reset(new UserSettings());
-        instance_->load(profile);
+    instance_.reset(new UserSettings());
+    instance_->load(profile);
 }
 
 void
 UserSettings::load(std::optional<QString> profile)
 {
-        tray_        = settings.value("user/window/tray", false).toBool();
-        startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
-
-        roomListWidth_      = settings.value("user/sidebar/room_list_width", -1).toInt();
-        communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt();
-
-        hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool();
-        hasAlertOnNotification_  = settings.value("user/alert_on_notification", false).toBool();
-        groupView_               = settings.value("user/group_view", true).toBool();
-        hiddenTags_              = settings.value("user/hidden_tags", QStringList{}).toStringList();
-        buttonsInTimeline_       = settings.value("user/timeline/buttons", true).toBool();
-        timelineMaxWidth_        = settings.value("user/timeline/max_width", 0).toInt();
-        messageHoverHighlight_ =
-          settings.value("user/timeline/message_hover_highlight", false).toBool();
-        enlargeEmojiOnlyMessages_ =
-          settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool();
-        markdown_             = settings.value("user/markdown_enabled", true).toBool();
-        typingNotifications_  = settings.value("user/typing_notifications", true).toBool();
-        sortByImportance_     = settings.value("user/sort_by_unread", true).toBool();
-        readReceipts_         = 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();
-        decryptSidebar_       = settings.value("user/decrypt_sidebar", true).toBool();
-        privacyScreen_        = settings.value("user/privacy_screen", false).toBool();
-        privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt();
-        mobileMode_           = settings.value("user/mobile_mode", false).toBool();
-        emojiFont_            = settings.value("user/emoji_font_family", "default").toString();
-        baseFontSize_         = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
-        auto tempPresence     = settings.value("user/presence", "").toString().toStdString();
-        auto presenceValue    = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
-        if (presenceValue < 0)
-                presenceValue = 0;
-        presence_               = static_cast<Presence>(presenceValue);
-        ringtone_               = settings.value("user/ringtone", "Default").toString();
-        microphone_             = settings.value("user/microphone", QString()).toString();
-        camera_                 = settings.value("user/camera", QString()).toString();
-        cameraResolution_       = settings.value("user/camera_resolution", QString()).toString();
-        cameraFrameRate_        = settings.value("user/camera_frame_rate", QString()).toString();
-        screenShareFrameRate_   = settings.value("user/screen_share_frame_rate", 5).toInt();
-        screenSharePiP_         = settings.value("user/screen_share_pip", true).toBool();
-        screenShareRemoteVideo_ = settings.value("user/screen_share_remote_video", false).toBool();
-        screenShareHideCursor_  = settings.value("user/screen_share_hide_cursor", false).toBool();
-        useStunServer_          = settings.value("user/use_stun_server", false).toBool();
-
-        if (profile) // set to "" if it's the default to maintain compatibility
-                profile_ = (*profile == "default") ? "" : *profile;
-        else
-                profile_ = settings.value("user/currentProfile", "").toString();
-
-        QString prefix =
-          (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : "";
-        accessToken_ = settings.value(prefix + "auth/access_token", "").toString();
-        homeserver_  = settings.value(prefix + "auth/home_server", "").toString();
-        userId_      = settings.value(prefix + "auth/user_id", "").toString();
-        deviceId_    = settings.value(prefix + "auth/device_id", "").toString();
-
-        shareKeysWithTrustedUsers_ =
-          settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false)
-            .toBool();
-        onlyShareKeysWithVerifiedUsers_ =
-          settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool();
-
-        disableCertificateValidation_ =
-          settings.value("disable_certificate_validation", false).toBool();
-
-        applyTheme();
+    tray_        = settings.value("user/window/tray", false).toBool();
+    startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
+
+    roomListWidth_      = settings.value("user/sidebar/room_list_width", -1).toInt();
+    communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt();
+
+    hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool();
+    hasAlertOnNotification_  = settings.value("user/alert_on_notification", false).toBool();
+    groupView_               = settings.value("user/group_view", true).toBool();
+    hiddenTags_              = settings.value("user/hidden_tags", QStringList{}).toStringList();
+    buttonsInTimeline_       = settings.value("user/timeline/buttons", true).toBool();
+    timelineMaxWidth_        = settings.value("user/timeline/max_width", 0).toInt();
+    messageHoverHighlight_ =
+      settings.value("user/timeline/message_hover_highlight", false).toBool();
+    enlargeEmojiOnlyMessages_ =
+      settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool();
+    markdown_             = settings.value("user/markdown_enabled", true).toBool();
+    animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool();
+    typingNotifications_  = settings.value("user/typing_notifications", true).toBool();
+    sortByImportance_     = settings.value("user/sort_by_unread", true).toBool();
+    readReceipts_         = 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();
+    useIdenticon_         = settings.value("user/use_identicon", true).toBool();
+    decryptSidebar_       = settings.value("user/decrypt_sidebar", true).toBool();
+    privacyScreen_        = settings.value("user/privacy_screen", false).toBool();
+    privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt();
+    mobileMode_           = settings.value("user/mobile_mode", false).toBool();
+    emojiFont_            = settings.value("user/emoji_font_family", "default").toString();
+    baseFontSize_         = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
+    auto tempPresence     = settings.value("user/presence", "").toString().toStdString();
+    auto presenceValue    = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
+    if (presenceValue < 0)
+        presenceValue = 0;
+    presence_               = static_cast<Presence>(presenceValue);
+    ringtone_               = settings.value("user/ringtone", "Default").toString();
+    microphone_             = settings.value("user/microphone", QString()).toString();
+    camera_                 = settings.value("user/camera", QString()).toString();
+    cameraResolution_       = settings.value("user/camera_resolution", QString()).toString();
+    cameraFrameRate_        = settings.value("user/camera_frame_rate", QString()).toString();
+    screenShareFrameRate_   = settings.value("user/screen_share_frame_rate", 5).toInt();
+    screenSharePiP_         = settings.value("user/screen_share_pip", true).toBool();
+    screenShareRemoteVideo_ = settings.value("user/screen_share_remote_video", false).toBool();
+    screenShareHideCursor_  = settings.value("user/screen_share_hide_cursor", false).toBool();
+    useStunServer_          = settings.value("user/use_stun_server", false).toBool();
+
+    if (profile) // set to "" if it's the default to maintain compatibility
+        profile_ = (*profile == "default") ? "" : *profile;
+    else
+        profile_ = settings.value("user/currentProfile", "").toString();
+
+    QString prefix = (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : "";
+    accessToken_   = settings.value(prefix + "auth/access_token", "").toString();
+    homeserver_    = settings.value(prefix + "auth/home_server", "").toString();
+    userId_        = settings.value(prefix + "auth/user_id", "").toString();
+    deviceId_      = settings.value(prefix + "auth/device_id", "").toString();
+
+    shareKeysWithTrustedUsers_ =
+      settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false).toBool();
+    onlyShareKeysWithVerifiedUsers_ =
+      settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool();
+    useOnlineKeyBackup_ = settings.value(prefix + "user/online_key_backup", false).toBool();
+
+    disableCertificateValidation_ =
+      settings.value("disable_certificate_validation", false).toBool();
+
+    applyTheme();
 }
 void
 UserSettings::setMessageHoverHighlight(bool state)
 {
-        if (state == messageHoverHighlight_)
-                return;
-        messageHoverHighlight_ = state;
-        emit messageHoverHighlightChanged(state);
-        save();
+    if (state == messageHoverHighlight_)
+        return;
+    messageHoverHighlight_ = state;
+    emit messageHoverHighlightChanged(state);
+    save();
 }
 void
 UserSettings::setEnlargeEmojiOnlyMessages(bool state)
 {
-        if (state == enlargeEmojiOnlyMessages_)
-                return;
-        enlargeEmojiOnlyMessages_ = state;
-        emit enlargeEmojiOnlyMessagesChanged(state);
-        save();
+    if (state == enlargeEmojiOnlyMessages_)
+        return;
+    enlargeEmojiOnlyMessages_ = state;
+    emit enlargeEmojiOnlyMessagesChanged(state);
+    save();
 }
 void
 UserSettings::setTray(bool state)
 {
-        if (state == tray_)
-                return;
-        tray_ = state;
-        emit trayChanged(state);
-        save();
+    if (state == tray_)
+        return;
+    tray_ = state;
+    emit trayChanged(state);
+    save();
 }
 
 void
 UserSettings::setStartInTray(bool state)
 {
-        if (state == startInTray_)
-                return;
-        startInTray_ = state;
-        emit startInTrayChanged(state);
-        save();
+    if (state == startInTray_)
+        return;
+    startInTray_ = state;
+    emit startInTrayChanged(state);
+    save();
 }
 
 void
 UserSettings::setMobileMode(bool state)
 {
-        if (state == mobileMode_)
-                return;
-        mobileMode_ = state;
-        emit mobileModeChanged(state);
-        save();
+    if (state == mobileMode_)
+        return;
+    mobileMode_ = state;
+    emit mobileModeChanged(state);
+    save();
 }
 
 void
 UserSettings::setGroupView(bool state)
 {
-        if (groupView_ == state)
-                return;
+    if (groupView_ == state)
+        return;
 
-        groupView_ = state;
-        emit groupViewStateChanged(state);
-        save();
+    groupView_ = state;
+    emit groupViewStateChanged(state);
+    save();
 }
 
 void
 UserSettings::setHiddenTags(QStringList hiddenTags)
 {
-        hiddenTags_ = hiddenTags;
-        save();
+    hiddenTags_ = hiddenTags;
+    save();
 }
 
 void
 UserSettings::setMarkdown(bool state)
 {
-        if (state == markdown_)
-                return;
-        markdown_ = state;
-        emit markdownChanged(state);
-        save();
+    if (state == markdown_)
+        return;
+    markdown_ = state;
+    emit markdownChanged(state);
+    save();
+}
+
+void
+UserSettings::setAnimateImagesOnHover(bool state)
+{
+    if (state == animateImagesOnHover_)
+        return;
+    animateImagesOnHover_ = state;
+    emit animateImagesOnHoverChanged(state);
+    save();
 }
 
 void
 UserSettings::setReadReceipts(bool state)
 {
-        if (state == readReceipts_)
-                return;
-        readReceipts_ = state;
-        emit readReceiptsChanged(state);
-        save();
+    if (state == readReceipts_)
+        return;
+    readReceipts_ = state;
+    emit readReceiptsChanged(state);
+    save();
 }
 
 void
 UserSettings::setTypingNotifications(bool state)
 {
-        if (state == typingNotifications_)
-                return;
-        typingNotifications_ = state;
-        emit typingNotificationsChanged(state);
-        save();
+    if (state == typingNotifications_)
+        return;
+    typingNotifications_ = state;
+    emit typingNotificationsChanged(state);
+    save();
 }
 
 void
 UserSettings::setSortByImportance(bool state)
 {
-        if (state == sortByImportance_)
-                return;
-        sortByImportance_ = state;
-        emit roomSortingChanged(state);
-        save();
+    if (state == sortByImportance_)
+        return;
+    sortByImportance_ = state;
+    emit roomSortingChanged(state);
+    save();
 }
 
 void
 UserSettings::setButtonsInTimeline(bool state)
 {
-        if (state == buttonsInTimeline_)
-                return;
-        buttonsInTimeline_ = state;
-        emit buttonInTimelineChanged(state);
-        save();
+    if (state == buttonsInTimeline_)
+        return;
+    buttonsInTimeline_ = state;
+    emit buttonInTimelineChanged(state);
+    save();
 }
 
 void
 UserSettings::setTimelineMaxWidth(int state)
 {
-        if (state == timelineMaxWidth_)
-                return;
-        timelineMaxWidth_ = state;
-        emit timelineMaxWidthChanged(state);
-        save();
+    if (state == timelineMaxWidth_)
+        return;
+    timelineMaxWidth_ = state;
+    emit timelineMaxWidthChanged(state);
+    save();
 }
 void
 UserSettings::setCommunityListWidth(int state)
 {
-        if (state == communityListWidth_)
-                return;
-        communityListWidth_ = state;
-        emit communityListWidthChanged(state);
-        save();
+    if (state == communityListWidth_)
+        return;
+    communityListWidth_ = state;
+    emit communityListWidthChanged(state);
+    save();
 }
 void
 UserSettings::setRoomListWidth(int state)
 {
-        if (state == roomListWidth_)
-                return;
-        roomListWidth_ = state;
-        emit roomListWidthChanged(state);
-        save();
+    if (state == roomListWidth_)
+        return;
+    roomListWidth_ = state;
+    emit roomListWidthChanged(state);
+    save();
 }
 
 void
 UserSettings::setDesktopNotifications(bool state)
 {
-        if (state == hasDesktopNotifications_)
-                return;
-        hasDesktopNotifications_ = state;
-        emit desktopNotificationsChanged(state);
-        save();
+    if (state == hasDesktopNotifications_)
+        return;
+    hasDesktopNotifications_ = state;
+    emit desktopNotificationsChanged(state);
+    save();
 }
 
 void
 UserSettings::setAlertOnNotification(bool state)
 {
-        if (state == hasAlertOnNotification_)
-                return;
-        hasAlertOnNotification_ = state;
-        emit alertOnNotificationChanged(state);
-        save();
+    if (state == hasAlertOnNotification_)
+        return;
+    hasAlertOnNotification_ = state;
+    emit alertOnNotificationChanged(state);
+    save();
 }
 
 void
 UserSettings::setAvatarCircles(bool state)
 {
-        if (state == avatarCircles_)
-                return;
-        avatarCircles_ = state;
-        emit avatarCirclesChanged(state);
-        save();
+    if (state == avatarCircles_)
+        return;
+    avatarCircles_ = state;
+    emit avatarCirclesChanged(state);
+    save();
 }
 
 void
 UserSettings::setDecryptSidebar(bool state)
 {
-        if (state == decryptSidebar_)
-                return;
-        decryptSidebar_ = state;
-        emit decryptSidebarChanged(state);
-        save();
+    if (state == decryptSidebar_)
+        return;
+    decryptSidebar_ = state;
+    emit decryptSidebarChanged(state);
+    save();
 }
 
 void
 UserSettings::setPrivacyScreen(bool state)
 {
-        if (state == privacyScreen_) {
-                return;
-        }
-        privacyScreen_ = state;
-        emit privacyScreenChanged(state);
-        save();
+    if (state == privacyScreen_) {
+        return;
+    }
+    privacyScreen_ = state;
+    emit privacyScreenChanged(state);
+    save();
 }
 
 void
 UserSettings::setPrivacyScreenTimeout(int state)
 {
-        if (state == privacyScreenTimeout_) {
-                return;
-        }
-        privacyScreenTimeout_ = state;
-        emit privacyScreenTimeoutChanged(state);
-        save();
+    if (state == privacyScreenTimeout_) {
+        return;
+    }
+    privacyScreenTimeout_ = state;
+    emit privacyScreenTimeoutChanged(state);
+    save();
 }
 
 void
 UserSettings::setFontSize(double size)
 {
-        if (size == baseFontSize_)
-                return;
-        baseFontSize_ = size;
-        emit fontSizeChanged(size);
-        save();
+    if (size == baseFontSize_)
+        return;
+    baseFontSize_ = size;
+    emit fontSizeChanged(size);
+    save();
 }
 
 void
 UserSettings::setFontFamily(QString family)
 {
-        if (family == font_)
-                return;
-        font_ = family;
-        emit fontChanged(family);
-        save();
+    if (family == font_)
+        return;
+    font_ = family;
+    emit fontChanged(family);
+    save();
 }
 
 void
 UserSettings::setEmojiFontFamily(QString family)
 {
-        if (family == emojiFont_)
-                return;
+    if (family == emojiFont_)
+        return;
 
-        if (family == tr("Default")) {
-                emojiFont_ = "default";
-        } else {
-                emojiFont_ = family;
-        }
+    if (family == tr("Default")) {
+        emojiFont_ = "default";
+    } else {
+        emojiFont_ = family;
+    }
 
-        emit emojiFontChanged(family);
-        save();
+    emit emojiFontChanged(family);
+    save();
 }
 
 void
 UserSettings::setPresence(Presence state)
 {
-        if (state == presence_)
-                return;
-        presence_ = state;
-        emit presenceChanged(state);
-        save();
+    if (state == presence_)
+        return;
+    presence_ = state;
+    emit presenceChanged(state);
+    save();
 }
 
 void
 UserSettings::setTheme(QString theme)
 {
-        if (theme == theme_)
-                return;
-        theme_ = theme;
-        save();
-        applyTheme();
-        emit themeChanged(theme);
+    if (theme == theme_)
+        return;
+    theme_ = theme;
+    save();
+    applyTheme();
+    emit themeChanged(theme);
 }
 
 void
 UserSettings::setUseStunServer(bool useStunServer)
 {
-        if (useStunServer == useStunServer_)
-                return;
-        useStunServer_ = useStunServer;
-        emit useStunServerChanged(useStunServer);
-        save();
+    if (useStunServer == useStunServer_)
+        return;
+    useStunServer_ = useStunServer;
+    emit useStunServerChanged(useStunServer);
+    save();
 }
 
 void
 UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys)
 {
-        if (shareKeys == onlyShareKeysWithVerifiedUsers_)
-                return;
+    if (shareKeys == onlyShareKeysWithVerifiedUsers_)
+        return;
 
-        onlyShareKeysWithVerifiedUsers_ = shareKeys;
-        emit onlyShareKeysWithVerifiedUsersChanged(shareKeys);
-        save();
+    onlyShareKeysWithVerifiedUsers_ = shareKeys;
+    emit onlyShareKeysWithVerifiedUsersChanged(shareKeys);
+    save();
 }
 
 void
 UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
 {
-        if (shareKeys == shareKeysWithTrustedUsers_)
-                return;
+    if (shareKeys == shareKeysWithTrustedUsers_)
+        return;
+
+    shareKeysWithTrustedUsers_ = shareKeys;
+    emit shareKeysWithTrustedUsersChanged(shareKeys);
+    save();
+}
+
+void
+UserSettings::setUseOnlineKeyBackup(bool useBackup)
+{
+    if (useBackup == useOnlineKeyBackup_)
+        return;
 
-        shareKeysWithTrustedUsers_ = shareKeys;
-        emit shareKeysWithTrustedUsersChanged(shareKeys);
-        save();
+    useOnlineKeyBackup_ = useBackup;
+    emit useOnlineKeyBackupChanged(useBackup);
+    save();
 }
 
 void
 UserSettings::setRingtone(QString ringtone)
 {
-        if (ringtone == ringtone_)
-                return;
-        ringtone_ = ringtone;
-        emit ringtoneChanged(ringtone);
-        save();
+    if (ringtone == ringtone_)
+        return;
+    ringtone_ = ringtone;
+    emit ringtoneChanged(ringtone);
+    save();
 }
 
 void
 UserSettings::setMicrophone(QString microphone)
 {
-        if (microphone == microphone_)
-                return;
-        microphone_ = microphone;
-        emit microphoneChanged(microphone);
-        save();
+    if (microphone == microphone_)
+        return;
+    microphone_ = microphone;
+    emit microphoneChanged(microphone);
+    save();
 }
 
 void
 UserSettings::setCamera(QString camera)
 {
-        if (camera == camera_)
-                return;
-        camera_ = camera;
-        emit cameraChanged(camera);
-        save();
+    if (camera == camera_)
+        return;
+    camera_ = camera;
+    emit cameraChanged(camera);
+    save();
 }
 
 void
 UserSettings::setCameraResolution(QString resolution)
 {
-        if (resolution == cameraResolution_)
-                return;
-        cameraResolution_ = resolution;
-        emit cameraResolutionChanged(resolution);
-        save();
+    if (resolution == cameraResolution_)
+        return;
+    cameraResolution_ = resolution;
+    emit cameraResolutionChanged(resolution);
+    save();
 }
 
 void
 UserSettings::setCameraFrameRate(QString frameRate)
 {
-        if (frameRate == cameraFrameRate_)
-                return;
-        cameraFrameRate_ = frameRate;
-        emit cameraFrameRateChanged(frameRate);
-        save();
+    if (frameRate == cameraFrameRate_)
+        return;
+    cameraFrameRate_ = frameRate;
+    emit cameraFrameRateChanged(frameRate);
+    save();
 }
 
 void
 UserSettings::setScreenShareFrameRate(int frameRate)
 {
-        if (frameRate == screenShareFrameRate_)
-                return;
-        screenShareFrameRate_ = frameRate;
-        emit screenShareFrameRateChanged(frameRate);
-        save();
+    if (frameRate == screenShareFrameRate_)
+        return;
+    screenShareFrameRate_ = frameRate;
+    emit screenShareFrameRateChanged(frameRate);
+    save();
 }
 
 void
 UserSettings::setScreenSharePiP(bool state)
 {
-        if (state == screenSharePiP_)
-                return;
-        screenSharePiP_ = state;
-        emit screenSharePiPChanged(state);
-        save();
+    if (state == screenSharePiP_)
+        return;
+    screenSharePiP_ = state;
+    emit screenSharePiPChanged(state);
+    save();
 }
 
 void
 UserSettings::setScreenShareRemoteVideo(bool state)
 {
-        if (state == screenShareRemoteVideo_)
-                return;
-        screenShareRemoteVideo_ = state;
-        emit screenShareRemoteVideoChanged(state);
-        save();
+    if (state == screenShareRemoteVideo_)
+        return;
+    screenShareRemoteVideo_ = state;
+    emit screenShareRemoteVideoChanged(state);
+    save();
 }
 
 void
 UserSettings::setScreenShareHideCursor(bool state)
 {
-        if (state == screenShareHideCursor_)
-                return;
-        screenShareHideCursor_ = state;
-        emit screenShareHideCursorChanged(state);
-        save();
+    if (state == screenShareHideCursor_)
+        return;
+    screenShareHideCursor_ = state;
+    emit screenShareHideCursorChanged(state);
+    save();
 }
 
 void
 UserSettings::setProfile(QString profile)
 {
-        if (profile == profile_)
-                return;
-        profile_ = profile;
-        emit profileChanged(profile_);
-        save();
+    if (profile == profile_)
+        return;
+    profile_ = profile;
+    emit profileChanged(profile_);
+    save();
 }
 
 void
 UserSettings::setUserId(QString userId)
 {
-        if (userId == userId_)
-                return;
-        userId_ = userId;
-        emit userIdChanged(userId_);
-        save();
+    if (userId == userId_)
+        return;
+    userId_ = userId;
+    emit userIdChanged(userId_);
+    save();
 }
 
 void
 UserSettings::setAccessToken(QString accessToken)
 {
-        if (accessToken == accessToken_)
-                return;
-        accessToken_ = accessToken;
-        emit accessTokenChanged(accessToken_);
-        save();
+    if (accessToken == accessToken_)
+        return;
+    accessToken_ = accessToken;
+    emit accessTokenChanged(accessToken_);
+    save();
 }
 
 void
 UserSettings::setDeviceId(QString deviceId)
 {
-        if (deviceId == deviceId_)
-                return;
-        deviceId_ = deviceId;
-        emit deviceIdChanged(deviceId_);
-        save();
+    if (deviceId == deviceId_)
+        return;
+    deviceId_ = deviceId;
+    emit deviceIdChanged(deviceId_);
+    save();
 }
 
 void
 UserSettings::setHomeserver(QString homeserver)
 {
-        if (homeserver == homeserver_)
-                return;
-        homeserver_ = homeserver;
-        emit homeserverChanged(homeserver_);
-        save();
+    if (homeserver == homeserver_)
+        return;
+    homeserver_ = homeserver;
+    emit homeserverChanged(homeserver_);
+    save();
 }
 
 void
 UserSettings::setDisableCertificateValidation(bool disabled)
 {
-        if (disabled == disableCertificateValidation_)
-                return;
-        disableCertificateValidation_ = disabled;
-        http::client()->verify_certificates(!disabled);
-        emit disableCertificateValidationChanged(disabled);
-        save();
+    if (disabled == disableCertificateValidation_)
+        return;
+    disableCertificateValidation_ = disabled;
+    http::client()->verify_certificates(!disabled);
+    emit disableCertificateValidationChanged(disabled);
+}
+
+void
+UserSettings::setUseIdenticon(bool state)
+{
+    if (state == useIdenticon_)
+        return;
+    useIdenticon_ = state;
+    emit useIdenticonChanged(useIdenticon_);
+    save();
 }
 
 void
 UserSettings::applyTheme()
 {
-        QFile stylefile;
+    QFile stylefile;
 
-        if (this->theme() == "light") {
-                stylefile.setFileName(":/styles/styles/nheko.qss");
-        } else if (this->theme() == "dark") {
-                stylefile.setFileName(":/styles/styles/nheko-dark.qss");
-        } else {
-                stylefile.setFileName(":/styles/styles/system.qss");
-        }
-        QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString()));
+    if (this->theme() == "light") {
+        stylefile.setFileName(":/styles/styles/nheko.qss");
+    } else if (this->theme() == "dark") {
+        stylefile.setFileName(":/styles/styles/nheko-dark.qss");
+    } else {
+        stylefile.setFileName(":/styles/styles/system.qss");
+    }
+    QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString()));
 
-        stylefile.open(QFile::ReadOnly);
-        QString stylesheet = QString(stylefile.readAll());
+    stylefile.open(QFile::ReadOnly);
+    QString stylesheet = QString(stylefile.readAll());
 
-        qobject_cast<QApplication *>(QApplication::instance())->setStyleSheet(stylesheet);
+    qobject_cast<QApplication *>(QApplication::instance())->setStyleSheet(stylesheet);
 }
 
 void
 UserSettings::save()
 {
-        settings.beginGroup("user");
-
-        settings.beginGroup("window");
-        settings.setValue("tray", tray_);
-        settings.setValue("start_in_tray", startInTray_);
-        settings.endGroup(); // window
-
-        settings.beginGroup("sidebar");
-        settings.setValue("community_list_width", communityListWidth_);
-        settings.setValue("room_list_width", roomListWidth_);
-        settings.endGroup(); // window
-
-        settings.beginGroup("timeline");
-        settings.setValue("buttons", buttonsInTimeline_);
-        settings.setValue("message_hover_highlight", messageHoverHighlight_);
-        settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_);
-        settings.setValue("max_width", timelineMaxWidth_);
-        settings.endGroup(); // timeline
-
-        settings.setValue("avatar_circles", avatarCircles_);
-        settings.setValue("decrypt_sidebar", decryptSidebar_);
-        settings.setValue("privacy_screen", privacyScreen_);
-        settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
-        settings.setValue("mobile_mode", mobileMode_);
-        settings.setValue("font_size", baseFontSize_);
-        settings.setValue("typing_notifications", typingNotifications_);
-        settings.setValue("sort_by_unread", sortByImportance_);
-        settings.setValue("minor_events", sortByImportance_);
-        settings.setValue("read_receipts", readReceipts_);
-        settings.setValue("group_view", groupView_);
-        settings.setValue("hidden_tags", hiddenTags_);
-        settings.setValue("markdown_enabled", markdown_);
-        settings.setValue("desktop_notifications", hasDesktopNotifications_);
-        settings.setValue("alert_on_notification", hasAlertOnNotification_);
-        settings.setValue("theme", theme());
-        settings.setValue("font_family", font_);
-        settings.setValue("emoji_font_family", emojiFont_);
-        settings.setValue("presence",
-                          QString::fromUtf8(QMetaEnum::fromType<Presence>().valueToKey(
-                            static_cast<int>(presence_))));
-        settings.setValue("ringtone", ringtone_);
-        settings.setValue("microphone", microphone_);
-        settings.setValue("camera", camera_);
-        settings.setValue("camera_resolution", cameraResolution_);
-        settings.setValue("camera_frame_rate", cameraFrameRate_);
-        settings.setValue("screen_share_frame_rate", screenShareFrameRate_);
-        settings.setValue("screen_share_pip", screenSharePiP_);
-        settings.setValue("screen_share_remote_video", screenShareRemoteVideo_);
-        settings.setValue("screen_share_hide_cursor", screenShareHideCursor_);
-        settings.setValue("use_stun_server", useStunServer_);
-        settings.setValue("currentProfile", profile_);
-
-        settings.endGroup(); // user
-
-        QString prefix =
-          (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : "";
-        settings.setValue(prefix + "auth/access_token", accessToken_);
-        settings.setValue(prefix + "auth/home_server", homeserver_);
-        settings.setValue(prefix + "auth/user_id", userId_);
-        settings.setValue(prefix + "auth/device_id", deviceId_);
-
-        settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users",
-                          shareKeysWithTrustedUsers_);
-        settings.setValue(prefix + "user/only_share_keys_with_verified_users",
-                          onlyShareKeysWithVerifiedUsers_);
-
-        settings.setValue("disable_certificate_validation", disableCertificateValidation_);
-
-        settings.sync();
+    settings.beginGroup("user");
+
+    settings.beginGroup("window");
+    settings.setValue("tray", tray_);
+    settings.setValue("start_in_tray", startInTray_);
+    settings.endGroup(); // window
+
+    settings.beginGroup("sidebar");
+    settings.setValue("community_list_width", communityListWidth_);
+    settings.setValue("room_list_width", roomListWidth_);
+    settings.endGroup(); // window
+
+    settings.beginGroup("timeline");
+    settings.setValue("buttons", buttonsInTimeline_);
+    settings.setValue("message_hover_highlight", messageHoverHighlight_);
+    settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_);
+    settings.setValue("max_width", timelineMaxWidth_);
+    settings.endGroup(); // timeline
+
+    settings.setValue("avatar_circles", avatarCircles_);
+    settings.setValue("decrypt_sidebar", decryptSidebar_);
+    settings.setValue("privacy_screen", privacyScreen_);
+    settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
+    settings.setValue("mobile_mode", mobileMode_);
+    settings.setValue("font_size", baseFontSize_);
+    settings.setValue("typing_notifications", typingNotifications_);
+    settings.setValue("sort_by_unread", sortByImportance_);
+    settings.setValue("minor_events", sortByImportance_);
+    settings.setValue("read_receipts", readReceipts_);
+    settings.setValue("group_view", groupView_);
+    settings.setValue("hidden_tags", hiddenTags_);
+    settings.setValue("markdown_enabled", markdown_);
+    settings.setValue("animate_images_on_hover", animateImagesOnHover_);
+    settings.setValue("desktop_notifications", hasDesktopNotifications_);
+    settings.setValue("alert_on_notification", hasAlertOnNotification_);
+    settings.setValue("theme", theme());
+    settings.setValue("font_family", font_);
+    settings.setValue("emoji_font_family", emojiFont_);
+    settings.setValue(
+      "presence",
+      QString::fromUtf8(QMetaEnum::fromType<Presence>().valueToKey(static_cast<int>(presence_))));
+    settings.setValue("ringtone", ringtone_);
+    settings.setValue("microphone", microphone_);
+    settings.setValue("camera", camera_);
+    settings.setValue("camera_resolution", cameraResolution_);
+    settings.setValue("camera_frame_rate", cameraFrameRate_);
+    settings.setValue("screen_share_frame_rate", screenShareFrameRate_);
+    settings.setValue("screen_share_pip", screenSharePiP_);
+    settings.setValue("screen_share_remote_video", screenShareRemoteVideo_);
+    settings.setValue("screen_share_hide_cursor", screenShareHideCursor_);
+    settings.setValue("use_stun_server", useStunServer_);
+    settings.setValue("currentProfile", profile_);
+    settings.setValue("use_identicon", useIdenticon_);
+
+    settings.endGroup(); // user
+
+    QString prefix = (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : "";
+    settings.setValue(prefix + "auth/access_token", accessToken_);
+    settings.setValue(prefix + "auth/home_server", homeserver_);
+    settings.setValue(prefix + "auth/user_id", userId_);
+    settings.setValue(prefix + "auth/device_id", deviceId_);
+
+    settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users",
+                      shareKeysWithTrustedUsers_);
+    settings.setValue(prefix + "user/only_share_keys_with_verified_users",
+                      onlyShareKeysWithVerifiedUsers_);
+    settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_);
+
+    settings.setValue("disable_certificate_validation", disableCertificateValidation_);
+
+    settings.sync();
 }
 
 HorizontalLine::HorizontalLine(QWidget *parent)
   : QFrame{parent}
 {
-        setFrameShape(QFrame::HLine);
-        setFrameShadow(QFrame::Sunken);
+    setFrameShape(QFrame::HLine);
+    setFrameShadow(QFrame::Sunken);
 }
 
 UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent)
   : QWidget{parent}
   , settings_{settings}
 {
-        topLayout_ = new QVBoxLayout{this};
-
-        QIcon icon;
-        icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png");
-
-        auto backBtn_ = new FlatButton{this};
-        backBtn_->setMinimumSize(QSize(24, 24));
-        backBtn_->setIcon(icon);
-        backBtn_->setIconSize(QSize(24, 24));
-
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * 1.1);
-
-        auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os));
-        if (QCoreApplication::applicationName() != "nheko")
-                versionInfo->setText(versionInfo->text() + " | " +
-                                     tr("profile: %1").arg(QCoreApplication::applicationName()));
-        versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction);
-
-        topBarLayout_ = new QHBoxLayout;
-        topBarLayout_->setSpacing(0);
-        topBarLayout_->setMargin(0);
-        topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter);
-        topBarLayout_->addStretch(1);
-
-        formLayout_ = new QFormLayout;
-
-        formLayout_->setLabelAlignment(Qt::AlignLeft);
-        formLayout_->setFormAlignment(Qt::AlignRight);
-        formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
-        formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows);
-        formLayout_->setHorizontalSpacing(0);
-
-        auto general_ = new QLabel{tr("GENERAL"), this};
-        general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
-        general_->setFont(font);
-
-        trayToggle_                     = new Toggle{this};
-        startInTrayToggle_              = new Toggle{this};
-        avatarCircles_                  = new Toggle{this};
-        decryptSidebar_                 = new Toggle(this);
-        privacyScreen_                  = new Toggle{this};
-        onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
-        shareKeysWithTrustedUsers_      = new Toggle(this);
-        groupViewToggle_                = new Toggle{this};
-        timelineButtonsToggle_          = new Toggle{this};
-        typingNotifications_            = new Toggle{this};
-        messageHoverHighlight_          = new Toggle{this};
-        enlargeEmojiOnlyMessages_       = new Toggle{this};
-        sortByImportance_               = new Toggle{this};
-        readReceipts_                   = new Toggle{this};
-        markdown_                       = new Toggle{this};
-        desktopNotifications_           = new Toggle{this};
-        alertOnNotification_            = new Toggle{this};
-        useStunServer_                  = new Toggle{this};
-        mobileMode_                     = new Toggle{this};
-        scaleFactorCombo_               = new QComboBox{this};
-        fontSizeCombo_                  = new QComboBox{this};
-        fontSelectionCombo_             = new QFontComboBox{this};
-        emojiFontSelectionCombo_        = new QComboBox{this};
-        ringtoneCombo_                  = new QComboBox{this};
-        microphoneCombo_                = new QComboBox{this};
-        cameraCombo_                    = new QComboBox{this};
-        cameraResolutionCombo_          = new QComboBox{this};
-        cameraFrameRateCombo_           = new QComboBox{this};
-        timelineMaxWidthSpin_           = new QSpinBox{this};
-        privacyScreenTimeout_           = new QSpinBox{this};
-
-        trayToggle_->setChecked(settings_->tray());
-        startInTrayToggle_->setChecked(settings_->startInTray());
-        avatarCircles_->setChecked(settings_->avatarCircles());
-        decryptSidebar_->setChecked(settings_->decryptSidebar());
-        privacyScreen_->setChecked(settings_->privacyScreen());
-        onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers());
-        shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
-        groupViewToggle_->setChecked(settings_->groupView());
-        timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
-        typingNotifications_->setChecked(settings_->typingNotifications());
-        messageHoverHighlight_->setChecked(settings_->messageHoverHighlight());
-        enlargeEmojiOnlyMessages_->setChecked(settings_->enlargeEmojiOnlyMessages());
-        sortByImportance_->setChecked(settings_->sortByImportance());
-        readReceipts_->setChecked(settings_->readReceipts());
-        markdown_->setChecked(settings_->markdown());
-        desktopNotifications_->setChecked(settings_->hasDesktopNotifications());
-        alertOnNotification_->setChecked(settings_->hasAlertOnNotification());
-        useStunServer_->setChecked(settings_->useStunServer());
-        mobileMode_->setChecked(settings_->mobileMode());
-
-        if (!settings_->tray()) {
-                startInTrayToggle_->setState(false);
-                startInTrayToggle_->setDisabled(true);
-        }
-
-        if (!settings_->privacyScreen()) {
-                privacyScreenTimeout_->setDisabled(true);
-        }
-
-        avatarCircles_->setFixedSize(64, 48);
-
-        auto uiLabel_ = new QLabel{tr("INTERFACE"), this};
-        uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin);
-        uiLabel_->setAlignment(Qt::AlignBottom);
-        uiLabel_->setFont(font);
-
-        for (double option = 1; option <= 3; option += 0.25)
-                scaleFactorCombo_->addItem(QString::number(option));
-        for (double option = 6; option <= 24; option += 0.5)
-                fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option)));
-
-        QFontDatabase fontDb;
-
-        // TODO: Is there a way to limit to just emojis, rather than
-        // all emoji fonts?
-        auto emojiFamilies = fontDb.families(QFontDatabase::Symbol);
-        emojiFontSelectionCombo_->addItem(tr("Default"));
-        for (const auto &family : emojiFamilies) {
-                emojiFontSelectionCombo_->addItem(family);
-        }
-
-        QString currentFont = settings_->font();
-        if (currentFont != "default" || currentFont != "") {
-                fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont));
+    topLayout_ = new QVBoxLayout{this};
+
+    QIcon icon;
+    icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png");
+
+    auto backBtn_ = new FlatButton{this};
+    backBtn_->setMinimumSize(QSize(24, 24));
+    backBtn_->setIcon(icon);
+    backBtn_->setIconSize(QSize(24, 24));
+
+    QFont font;
+    font.setPointSizeF(font.pointSizeF() * 1.1);
+
+    auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os));
+    if (QCoreApplication::applicationName() != "nheko")
+        versionInfo->setText(versionInfo->text() + " | " +
+                             tr("profile: %1").arg(QCoreApplication::applicationName()));
+    versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction);
+
+    topBarLayout_ = new QHBoxLayout;
+    topBarLayout_->setSpacing(0);
+    topBarLayout_->setMargin(0);
+    topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter);
+    topBarLayout_->addStretch(1);
+
+    formLayout_ = new QFormLayout;
+
+    formLayout_->setLabelAlignment(Qt::AlignLeft);
+    formLayout_->setFormAlignment(Qt::AlignRight);
+    formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
+    formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows);
+    formLayout_->setHorizontalSpacing(0);
+
+    auto general_ = new QLabel{tr("GENERAL"), this};
+    general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
+    general_->setFont(font);
+
+    trayToggle_                     = new Toggle{this};
+    startInTrayToggle_              = new Toggle{this};
+    avatarCircles_                  = new Toggle{this};
+    useIdenticon_                   = new Toggle{this};
+    decryptSidebar_                 = new Toggle(this);
+    privacyScreen_                  = new Toggle{this};
+    onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
+    shareKeysWithTrustedUsers_      = new Toggle(this);
+    useOnlineKeyBackup_             = new Toggle(this);
+    groupViewToggle_                = new Toggle{this};
+    timelineButtonsToggle_          = new Toggle{this};
+    typingNotifications_            = new Toggle{this};
+    messageHoverHighlight_          = new Toggle{this};
+    enlargeEmojiOnlyMessages_       = new Toggle{this};
+    sortByImportance_               = new Toggle{this};
+    readReceipts_                   = new Toggle{this};
+    markdown_                       = new Toggle{this};
+    animateImagesOnHover_           = new Toggle{this};
+    desktopNotifications_           = new Toggle{this};
+    alertOnNotification_            = new Toggle{this};
+    useStunServer_                  = new Toggle{this};
+    mobileMode_                     = new Toggle{this};
+    scaleFactorCombo_               = new QComboBox{this};
+    fontSizeCombo_                  = new QComboBox{this};
+    fontSelectionCombo_             = new QFontComboBox{this};
+    emojiFontSelectionCombo_        = new QComboBox{this};
+    ringtoneCombo_                  = new QComboBox{this};
+    microphoneCombo_                = new QComboBox{this};
+    cameraCombo_                    = new QComboBox{this};
+    cameraResolutionCombo_          = new QComboBox{this};
+    cameraFrameRateCombo_           = new QComboBox{this};
+    timelineMaxWidthSpin_           = new QSpinBox{this};
+    privacyScreenTimeout_           = new QSpinBox{this};
+
+    trayToggle_->setChecked(settings_->tray());
+    startInTrayToggle_->setChecked(settings_->startInTray());
+    avatarCircles_->setChecked(settings_->avatarCircles());
+    useIdenticon_->setChecked(settings_->useIdenticon());
+    decryptSidebar_->setChecked(settings_->decryptSidebar());
+    privacyScreen_->setChecked(settings_->privacyScreen());
+    onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers());
+    shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
+    useOnlineKeyBackup_->setChecked(settings_->useOnlineKeyBackup());
+    groupViewToggle_->setChecked(settings_->groupView());
+    timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
+    typingNotifications_->setChecked(settings_->typingNotifications());
+    messageHoverHighlight_->setChecked(settings_->messageHoverHighlight());
+    enlargeEmojiOnlyMessages_->setChecked(settings_->enlargeEmojiOnlyMessages());
+    sortByImportance_->setChecked(settings_->sortByImportance());
+    readReceipts_->setChecked(settings_->readReceipts());
+    markdown_->setChecked(settings_->markdown());
+    animateImagesOnHover_->setChecked(settings_->animateImagesOnHover());
+    desktopNotifications_->setChecked(settings_->hasDesktopNotifications());
+    alertOnNotification_->setChecked(settings_->hasAlertOnNotification());
+    useStunServer_->setChecked(settings_->useStunServer());
+    mobileMode_->setChecked(settings_->mobileMode());
+
+    if (!settings_->tray()) {
+        startInTrayToggle_->setState(false);
+        startInTrayToggle_->setDisabled(true);
+    }
+
+    if (!settings_->privacyScreen()) {
+        privacyScreenTimeout_->setDisabled(true);
+    }
+
+    avatarCircles_->setFixedSize(64, 48);
+
+    auto uiLabel_ = new QLabel{tr("INTERFACE"), this};
+    uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin);
+    uiLabel_->setAlignment(Qt::AlignBottom);
+    uiLabel_->setFont(font);
+
+    for (double option = 1; option <= 3; option += 0.25)
+        scaleFactorCombo_->addItem(QString::number(option));
+    for (double option = 6; option <= 24; option += 0.5)
+        fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option)));
+
+    QFontDatabase fontDb;
+
+    // TODO: Is there a way to limit to just emojis, rather than
+    // all emoji fonts?
+    auto emojiFamilies = fontDb.families(QFontDatabase::Symbol);
+    emojiFontSelectionCombo_->addItem(tr("Default"));
+    for (const auto &family : emojiFamilies) {
+        emojiFontSelectionCombo_->addItem(family);
+    }
+
+    QString currentFont = settings_->font();
+    if (currentFont != "default" || currentFont != "") {
+        fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont));
+    }
+
+    emojiFontSelectionCombo_->setCurrentIndex(
+      emojiFontSelectionCombo_->findText(settings_->emojiFont()));
+
+    themeCombo_ = new QComboBox{this};
+    themeCombo_->addItem("Light");
+    themeCombo_->addItem("Dark");
+    themeCombo_->addItem("System");
+
+    QString themeStr = settings_->theme();
+    themeStr.replace(0, 1, themeStr[0].toUpper());
+    int themeIndex = themeCombo_->findText(themeStr);
+    themeCombo_->setCurrentIndex(themeIndex);
+
+    timelineMaxWidthSpin_->setMinimum(0);
+    timelineMaxWidthSpin_->setMaximum(100'000'000);
+    timelineMaxWidthSpin_->setSingleStep(10);
+
+    privacyScreenTimeout_->setMinimum(0);
+    privacyScreenTimeout_->setMaximum(3600);
+    privacyScreenTimeout_->setSingleStep(10);
+
+    auto callsLabel = new QLabel{tr("CALLS"), this};
+    callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin);
+    callsLabel->setAlignment(Qt::AlignBottom);
+    callsLabel->setFont(font);
+
+    auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this};
+    encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin);
+    encryptionLabel_->setAlignment(Qt::AlignBottom);
+    encryptionLabel_->setFont(font);
+
+    QFont monospaceFont;
+    monospaceFont.setFamily("Monospace");
+    monospaceFont.setStyleHint(QFont::Monospace);
+    monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9);
+
+    deviceIdValue_ = new QLabel{this};
+    deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse);
+    deviceIdValue_->setFont(monospaceFont);
+
+    deviceFingerprintValue_ = new QLabel{this};
+    deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse);
+    deviceFingerprintValue_->setFont(monospaceFont);
+
+    deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X')));
+
+    backupSecretCached      = new QLabel{this};
+    masterSecretCached      = new QLabel{this};
+    selfSigningSecretCached = new QLabel{this};
+    userSigningSecretCached = new QLabel{this};
+    backupSecretCached->setFont(monospaceFont);
+    masterSecretCached->setFont(monospaceFont);
+    selfSigningSecretCached->setFont(monospaceFont);
+    userSigningSecretCached->setFont(monospaceFont);
+
+    auto sessionKeysLabel = new QLabel{tr("Session Keys"), this};
+    sessionKeysLabel->setFont(font);
+    sessionKeysLabel->setMargin(OptionMargin);
+
+    auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this};
+    auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this};
+
+    auto sessionKeysLayout = new QHBoxLayout;
+    sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight);
+    sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight);
+    sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight);
+
+    auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this};
+    crossSigningKeysLabel->setFont(font);
+    crossSigningKeysLabel->setMargin(OptionMargin);
+
+    auto crossSigningRequestBtn  = new QPushButton{tr("REQUEST"), this};
+    auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this};
+
+    auto crossSigningKeysLayout = new QHBoxLayout;
+    crossSigningKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight);
+    crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight);
+    crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight);
+
+    auto boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") {
+        auto label = new QLabel{labelText, this};
+        label->setFont(font);
+        label->setMargin(OptionMargin);
+
+        if (!tooltipText.isEmpty()) {
+            label->setToolTip(tooltipText);
         }
 
-        emojiFontSelectionCombo_->setCurrentIndex(
-          emojiFontSelectionCombo_->findText(settings_->emojiFont()));
-
-        themeCombo_ = new QComboBox{this};
-        themeCombo_->addItem("Light");
-        themeCombo_->addItem("Dark");
-        themeCombo_->addItem("System");
-
-        QString themeStr = settings_->theme();
-        themeStr.replace(0, 1, themeStr[0].toUpper());
-        int themeIndex = themeCombo_->findText(themeStr);
-        themeCombo_->setCurrentIndex(themeIndex);
-
-        timelineMaxWidthSpin_->setMinimum(0);
-        timelineMaxWidthSpin_->setMaximum(100'000'000);
-        timelineMaxWidthSpin_->setSingleStep(10);
-
-        privacyScreenTimeout_->setMinimum(0);
-        privacyScreenTimeout_->setMaximum(3600);
-        privacyScreenTimeout_->setSingleStep(10);
-
-        auto callsLabel = new QLabel{tr("CALLS"), this};
-        callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin);
-        callsLabel->setAlignment(Qt::AlignBottom);
-        callsLabel->setFont(font);
-
-        auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this};
-        encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin);
-        encryptionLabel_->setAlignment(Qt::AlignBottom);
-        encryptionLabel_->setFont(font);
-
-        QFont monospaceFont;
-        monospaceFont.setFamily("Monospace");
-        monospaceFont.setStyleHint(QFont::Monospace);
-        monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9);
-
-        deviceIdValue_ = new QLabel{this};
-        deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse);
-        deviceIdValue_->setFont(monospaceFont);
-
-        deviceFingerprintValue_ = new QLabel{this};
-        deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse);
-        deviceFingerprintValue_->setFont(monospaceFont);
-
-        deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X')));
-
-        backupSecretCached      = new QLabel{this};
-        masterSecretCached      = new QLabel{this};
-        selfSigningSecretCached = new QLabel{this};
-        userSigningSecretCached = new QLabel{this};
-        backupSecretCached->setFont(monospaceFont);
-        masterSecretCached->setFont(monospaceFont);
-        selfSigningSecretCached->setFont(monospaceFont);
-        userSigningSecretCached->setFont(monospaceFont);
-
-        auto sessionKeysLabel = new QLabel{tr("Session Keys"), this};
-        sessionKeysLabel->setFont(font);
-        sessionKeysLabel->setMargin(OptionMargin);
-
-        auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this};
-        auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this};
-
-        auto sessionKeysLayout = new QHBoxLayout;
-        sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight);
-        sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight);
-        sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight);
-
-        auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this};
-        crossSigningKeysLabel->setFont(font);
-        crossSigningKeysLabel->setMargin(OptionMargin);
-
-        auto crossSigningRequestBtn  = new QPushButton{tr("REQUEST"), this};
-        auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this};
-
-        auto crossSigningKeysLayout = new QHBoxLayout;
-        crossSigningKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight);
-        crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight);
-        crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight);
-
-        auto boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") {
-                auto label = new QLabel{labelText, this};
-                label->setFont(font);
-                label->setMargin(OptionMargin);
-
-                if (!tooltipText.isEmpty()) {
-                        label->setToolTip(tooltipText);
-                }
-
-                auto layout = new QHBoxLayout;
-                layout->addWidget(field, 0, Qt::AlignRight);
-
-                formLayout_->addRow(label, layout);
-        };
-
-        formLayout_->addRow(general_);
-        formLayout_->addRow(new HorizontalLine{this});
-        boxWrap(
-          tr("Minimize to tray"),
-          trayToggle_,
-          tr("Keep the application running in the background after closing the client window."));
-        boxWrap(tr("Start in tray"),
-                startInTrayToggle_,
-                tr("Start the application in the background without showing the client window."));
-        formLayout_->addRow(new HorizontalLine{this});
-        boxWrap(tr("Circular Avatars"),
-                avatarCircles_,
-                tr("Change the appearance of user avatars in chats.\nOFF - square, ON - Circle."));
-        boxWrap(tr("Group's sidebar"),
-                groupViewToggle_,
-                tr("Show a column containing groups and tags next to the room list."));
-        boxWrap(tr("Decrypt messages in sidebar"),
-                decryptSidebar_,
-                tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in "
-                   "encrypted chats."));
-        boxWrap(tr("Privacy Screen"),
-                privacyScreen_,
-                tr("When the window loses focus, the timeline will\nbe blurred."));
-        boxWrap(
-          tr("Privacy screen timeout (in seconds [0 - 3600])"),
-          privacyScreenTimeout_,
-          tr("Set timeout (in seconds) for how long after window loses\nfocus before the screen"
-             " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 "
-             "hour (3600 seconds)"));
-        boxWrap(tr("Show buttons in timeline"),
-                timelineButtonsToggle_,
-                tr("Show buttons to quickly reply, react or access additional options next to each "
-                   "message."));
-        boxWrap(tr("Limit width of timeline"),
-                timelineMaxWidthSpin_,
-                tr("Set the max width of messages in the timeline (in pixels). This can help "
-                   "readability on wide screen, when Nheko is maximised"));
-        boxWrap(tr("Typing notifications"),
-                typingNotifications_,
-                tr("Show who is typing in a room.\nThis will also enable or disable sending typing "
-                   "notifications to others."));
-        boxWrap(
-          tr("Sort rooms by unreads"),
-          sortByImportance_,
-          tr(
-            "Display rooms with new messages first.\nIf this is off, the list of rooms will only "
-            "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms which "
-            "have active notifications (the small circle with a number in it) will be sorted on "
-            "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't "
-            "seem to consider them as important as the other rooms."));
-        formLayout_->addRow(new HorizontalLine{this});
-        boxWrap(tr("Read receipts"),
-                readReceipts_,
-                tr("Show if your message was read.\nStatus is displayed next to timestamps."));
-        boxWrap(
-          tr("Send messages as Markdown"),
-          markdown_,
-          tr("Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
-             "text."));
-        boxWrap(tr("Desktop notifications"),
-                desktopNotifications_,
-                tr("Notify about received message when the client is not currently focused."));
-        boxWrap(tr("Alert on notification"),
-                alertOnNotification_,
-                tr("Show an alert when a message is received.\nThis usually causes the application "
-                   "icon in the task bar to animate in some fashion."));
-        boxWrap(tr("Highlight message on hover"),
-                messageHoverHighlight_,
-                tr("Change the background color of messages when you hover over them."));
-        boxWrap(tr("Large Emoji in timeline"),
-                enlargeEmojiOnlyMessages_,
-                tr("Make font size larger if messages with only a few emojis are displayed."));
-        formLayout_->addRow(uiLabel_);
-        formLayout_->addRow(new HorizontalLine{this});
-
-        boxWrap(tr("Touchscreen mode"),
-                mobileMode_,
-                tr("Will prevent text selection in the timeline to make touch scrolling easier."));
+        auto layout = new QHBoxLayout;
+        layout->addWidget(field, 0, Qt::AlignRight);
+
+        formLayout_->addRow(label, layout);
+    };
+
+    formLayout_->addRow(general_);
+    formLayout_->addRow(new HorizontalLine{this});
+    boxWrap(tr("Minimize to tray"),
+            trayToggle_,
+            tr("Keep the application running in the background after closing the client window."));
+    boxWrap(tr("Start in tray"),
+            startInTrayToggle_,
+            tr("Start the application in the background without showing the client window."));
+    formLayout_->addRow(new HorizontalLine{this});
+    boxWrap(tr("Circular Avatars"),
+            avatarCircles_,
+            tr("Change the appearance of user avatars in chats.\nOFF - square, ON - Circle."));
+    boxWrap(tr("Use identicons"),
+            useIdenticon_,
+            tr("Display an identicon instead of a letter when a user has not set an avatar."));
+    boxWrap(tr("Group's sidebar"),
+            groupViewToggle_,
+            tr("Show a column containing groups and tags next to the room list."));
+    boxWrap(tr("Decrypt messages in sidebar"),
+            decryptSidebar_,
+            tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in "
+               "encrypted chats."));
+    boxWrap(tr("Privacy Screen"),
+            privacyScreen_,
+            tr("When the window loses focus, the timeline will\nbe blurred."));
+    boxWrap(tr("Privacy screen timeout (in seconds [0 - 3600])"),
+            privacyScreenTimeout_,
+            tr("Set timeout (in seconds) for how long after window loses\nfocus before the screen"
+               " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 "
+               "hour (3600 seconds)"));
+    boxWrap(tr("Show buttons in timeline"),
+            timelineButtonsToggle_,
+            tr("Show buttons to quickly reply, react or access additional options next to each "
+               "message."));
+    boxWrap(tr("Limit width of timeline"),
+            timelineMaxWidthSpin_,
+            tr("Set the max width of messages in the timeline (in pixels). This can help "
+               "readability on wide screen, when Nheko is maximised"));
+    boxWrap(tr("Typing notifications"),
+            typingNotifications_,
+            tr("Show who is typing in a room.\nThis will also enable or disable sending typing "
+               "notifications to others."));
+    boxWrap(
+      tr("Sort rooms by unreads"),
+      sortByImportance_,
+      tr("Display rooms with new messages first.\nIf this is off, the list of rooms will only "
+         "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms which "
+         "have active notifications (the small circle with a number in it) will be sorted on "
+         "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't "
+         "seem to consider them as important as the other rooms."));
+    formLayout_->addRow(new HorizontalLine{this});
+    boxWrap(tr("Read receipts"),
+            readReceipts_,
+            tr("Show if your message was read.\nStatus is displayed next to timestamps."));
+    boxWrap(tr("Send messages as Markdown"),
+            markdown_,
+            tr("Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
+               "text."));
+    boxWrap(tr("Play animated images only on hover"),
+            animateImagesOnHover_,
+            tr("Plays media like GIFs or WEBPs only when explicitly hovering over them."));
+    boxWrap(tr("Desktop notifications"),
+            desktopNotifications_,
+            tr("Notify about received message when the client is not currently focused."));
+    boxWrap(tr("Alert on notification"),
+            alertOnNotification_,
+            tr("Show an alert when a message is received.\nThis usually causes the application "
+               "icon in the task bar to animate in some fashion."));
+    boxWrap(tr("Highlight message on hover"),
+            messageHoverHighlight_,
+            tr("Change the background color of messages when you hover over them."));
+    boxWrap(tr("Large Emoji in timeline"),
+            enlargeEmojiOnlyMessages_,
+            tr("Make font size larger if messages with only a few emojis are displayed."));
+    formLayout_->addRow(uiLabel_);
+    formLayout_->addRow(new HorizontalLine{this});
+
+    boxWrap(tr("Touchscreen mode"),
+            mobileMode_,
+            tr("Will prevent text selection in the timeline to make touch scrolling easier."));
 #if !defined(Q_OS_MAC)
-        boxWrap(tr("Scale factor"),
-                scaleFactorCombo_,
-                tr("Change the scale factor of the whole user interface."));
+    boxWrap(tr("Scale factor"),
+            scaleFactorCombo_,
+            tr("Change the scale factor of the whole user interface."));
 #else
-        scaleFactorCombo_->hide();
+    scaleFactorCombo_->hide();
 #endif
-        boxWrap(tr("Font size"), fontSizeCombo_);
-        boxWrap(tr("Font Family"), fontSelectionCombo_);
+    boxWrap(tr("Font size"), fontSizeCombo_);
+    boxWrap(tr("Font Family"), fontSelectionCombo_);
 
 #if !defined(Q_OS_MAC)
-        boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_);
+    boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_);
 #else
-        emojiFontSelectionCombo_->hide();
+    emojiFontSelectionCombo_->hide();
 #endif
 
-        boxWrap(tr("Theme"), themeCombo_);
-
-        formLayout_->addRow(callsLabel);
-        formLayout_->addRow(new HorizontalLine{this});
-        boxWrap(tr("Ringtone"),
-                ringtoneCombo_,
-                tr("Set the notification sound to play when a call invite arrives"));
-        boxWrap(tr("Microphone"), microphoneCombo_);
-        boxWrap(tr("Camera"), cameraCombo_);
-        boxWrap(tr("Camera resolution"), cameraResolutionCombo_);
-        boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_);
-
-        ringtoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
-        ringtoneCombo_->addItem("Mute");
-        ringtoneCombo_->addItem("Default");
-        ringtoneCombo_->addItem("Other...");
-        const QString &ringtone = settings_->ringtone();
-        if (!ringtone.isEmpty() && ringtone != "Mute" && ringtone != "Default")
-                ringtoneCombo_->addItem(ringtone);
-        microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
-        cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
-        cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
-        cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
-
-        boxWrap(tr("Allow fallback call assist server"),
-                useStunServer_,
-                tr("Will use turn.matrix.org as assist when your home server does not offer one."));
-
-        formLayout_->addRow(encryptionLabel_);
-        formLayout_->addRow(new HorizontalLine{this});
-        boxWrap(tr("Device ID"), deviceIdValue_);
-        boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
-        boxWrap(tr("Send encrypted messages to verified users only"),
-                onlyShareKeysWithVerifiedUsers_,
-                tr("Requires a user to be verified to send encrypted messages to them. This "
-                   "improves safety but makes E2EE more tedious."));
-        boxWrap(tr("Share keys with verified users and devices"),
-                shareKeysWithTrustedUsers_,
-                tr("Automatically replies to key requests from other users, if they are verified, "
-                   "even if that device shouldn't have access to those keys otherwise."));
-        formLayout_->addRow(new HorizontalLine{this});
-        formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
-        formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
-
-        boxWrap(tr("Master signing key"),
-                masterSecretCached,
-                tr("Your most important key. You don't need to have it cached, since not caching "
-                   "it makes it less likely it can be stolen and it is only needed to rotate your "
-                   "other signing keys."));
-        boxWrap(tr("User signing key"),
-                userSigningSecretCached,
-                tr("The key to verify other users. If it is cached, verifying a user will verify "
-                   "all their devices."));
-        boxWrap(
-          tr("Self signing key"),
-          selfSigningSecretCached,
-          tr("The key to verify your own devices. If it is cached, verifying one of your devices "
-             "will mark it verified for all your other devices and for users, that have verified "
-             "you."));
-        boxWrap(tr("Backup key"),
-                backupSecretCached,
-                tr("The key to decrypt online key backups. If it is cached, you can enable online "
-                   "key backup to store encryption keys securely encrypted on the server."));
-        updateSecretStatus();
-
-        auto scrollArea_ = new QScrollArea{this};
-        scrollArea_->setFrameShape(QFrame::NoFrame);
-        scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-        scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
-        scrollArea_->setWidgetResizable(true);
-        scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter);
-
-        QScroller::grabGesture(scrollArea_, QScroller::TouchGesture);
-
-        auto spacingAroundForm = new QHBoxLayout;
-        spacingAroundForm->addStretch(1);
-        spacingAroundForm->addLayout(formLayout_, 0);
-        spacingAroundForm->addStretch(1);
-
-        auto scrollAreaContents_ = new QWidget{this};
-        scrollAreaContents_->setObjectName("UserSettingScrollWidget");
-        scrollAreaContents_->setLayout(spacingAroundForm);
-
-        scrollArea_->setWidget(scrollAreaContents_);
-        topLayout_->addLayout(topBarLayout_);
-        topLayout_->addWidget(scrollArea_, Qt::AlignTop);
-        topLayout_->addStretch(1);
-        topLayout_->addWidget(versionInfo);
-
-        connect(themeCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &text) {
-                        settings_->setTheme(text.toLower());
-                        emit themeChanged();
-                });
-        connect(scaleFactorCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); });
-        connect(fontSizeCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); });
-        connect(fontSelectionCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &family) { settings_->setFontFamily(family.trimmed()); });
-        connect(emojiFontSelectionCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); });
-
-        connect(ringtoneCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &ringtone) {
-                        if (ringtone == "Other...") {
-                                QString homeFolder =
-                                  QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
-                                auto filepath = QFileDialog::getOpenFileName(
-                                  this, tr("Select a file"), homeFolder, tr("All Files (*)"));
-                                if (!filepath.isEmpty()) {
-                                        const auto &oldSetting = settings_->ringtone();
-                                        if (oldSetting != "Mute" && oldSetting != "Default")
-                                                ringtoneCombo_->removeItem(
-                                                  ringtoneCombo_->findText(oldSetting));
-                                        settings_->setRingtone(filepath);
-                                        ringtoneCombo_->addItem(filepath);
-                                        ringtoneCombo_->setCurrentText(filepath);
-                                } else {
-                                        ringtoneCombo_->setCurrentText(settings_->ringtone());
-                                }
-                        } else if (ringtone == "Mute" || ringtone == "Default") {
-                                const auto &oldSetting = settings_->ringtone();
-                                if (oldSetting != "Mute" && oldSetting != "Default")
-                                        ringtoneCombo_->removeItem(
-                                          ringtoneCombo_->findText(oldSetting));
-                                settings_->setRingtone(ringtone);
-                        }
-                });
-
-        connect(microphoneCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &microphone) { settings_->setMicrophone(microphone); });
-
-        connect(cameraCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &camera) {
-                        settings_->setCamera(camera);
-                        std::vector<std::string> resolutions =
-                          CallDevices::instance().resolutions(camera.toStdString());
-                        cameraResolutionCombo_->clear();
-                        for (const auto &resolution : resolutions)
-                                cameraResolutionCombo_->addItem(QString::fromStdString(resolution));
-                });
-
-        connect(cameraResolutionCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &resolution) {
-                        settings_->setCameraResolution(resolution);
-                        std::vector<std::string> frameRates = CallDevices::instance().frameRates(
-                          settings_->camera().toStdString(), resolution.toStdString());
-                        cameraFrameRateCombo_->clear();
-                        for (const auto &frameRate : frameRates)
-                                cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate));
-                });
-
-        connect(cameraFrameRateCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); });
-
-        connect(trayToggle_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setTray(enabled);
-                if (enabled) {
-                        startInTrayToggle_->setChecked(false);
-                        startInTrayToggle_->setEnabled(true);
-                        startInTrayToggle_->setState(false);
-                        settings_->setStartInTray(false);
-                } else {
-                        startInTrayToggle_->setChecked(false);
-                        startInTrayToggle_->setState(false);
-                        startInTrayToggle_->setDisabled(true);
-                        settings_->setStartInTray(false);
+    boxWrap(tr("Theme"), themeCombo_);
+
+    formLayout_->addRow(callsLabel);
+    formLayout_->addRow(new HorizontalLine{this});
+    boxWrap(tr("Ringtone"),
+            ringtoneCombo_,
+            tr("Set the notification sound to play when a call invite arrives"));
+    boxWrap(tr("Microphone"), microphoneCombo_);
+    boxWrap(tr("Camera"), cameraCombo_);
+    boxWrap(tr("Camera resolution"), cameraResolutionCombo_);
+    boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_);
+
+    ringtoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+    ringtoneCombo_->addItem("Mute");
+    ringtoneCombo_->addItem("Default");
+    ringtoneCombo_->addItem("Other...");
+    const QString &ringtone = settings_->ringtone();
+    if (!ringtone.isEmpty() && ringtone != "Mute" && ringtone != "Default")
+        ringtoneCombo_->addItem(ringtone);
+    microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+    cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+    cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+    cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+
+    boxWrap(tr("Allow fallback call assist server"),
+            useStunServer_,
+            tr("Will use turn.matrix.org as assist when your home server does not offer one."));
+
+    formLayout_->addRow(encryptionLabel_);
+    formLayout_->addRow(new HorizontalLine{this});
+    boxWrap(tr("Device ID"), deviceIdValue_);
+    boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
+    boxWrap(tr("Send encrypted messages to verified users only"),
+            onlyShareKeysWithVerifiedUsers_,
+            tr("Requires a user to be verified to send encrypted messages to them. This "
+               "improves safety but makes E2EE more tedious."));
+    boxWrap(tr("Share keys with verified users and devices"),
+            shareKeysWithTrustedUsers_,
+            tr("Automatically replies to key requests from other users, if they are verified, "
+               "even if that device shouldn't have access to those keys otherwise."));
+    boxWrap(tr("Online Key Backup"),
+            useOnlineKeyBackup_,
+            tr("Download message encryption keys from and upload to the encrypted online key "
+               "backup."));
+    formLayout_->addRow(new HorizontalLine{this});
+    formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
+    formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
+
+    boxWrap(tr("Master signing key"),
+            masterSecretCached,
+            tr("Your most important key. You don't need to have it cached, since not caching "
+               "it makes it less likely it can be stolen and it is only needed to rotate your "
+               "other signing keys."));
+    boxWrap(tr("User signing key"),
+            userSigningSecretCached,
+            tr("The key to verify other users. If it is cached, verifying a user will verify "
+               "all their devices."));
+    boxWrap(tr("Self signing key"),
+            selfSigningSecretCached,
+            tr("The key to verify your own devices. If it is cached, verifying one of your devices "
+               "will mark it verified for all your other devices and for users, that have verified "
+               "you."));
+    boxWrap(tr("Backup key"),
+            backupSecretCached,
+            tr("The key to decrypt online key backups. If it is cached, you can enable online "
+               "key backup to store encryption keys securely encrypted on the server."));
+    updateSecretStatus();
+
+    auto scrollArea_ = new QScrollArea{this};
+    scrollArea_->setFrameShape(QFrame::NoFrame);
+    scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+    scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+    scrollArea_->setWidgetResizable(true);
+    scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter);
+
+    QScroller::grabGesture(scrollArea_, QScroller::TouchGesture);
+
+    auto spacingAroundForm = new QHBoxLayout;
+    spacingAroundForm->addStretch(1);
+    spacingAroundForm->addLayout(formLayout_, 0);
+    spacingAroundForm->addStretch(1);
+
+    auto scrollAreaContents_ = new QWidget{this};
+    scrollAreaContents_->setObjectName("UserSettingScrollWidget");
+    scrollAreaContents_->setLayout(spacingAroundForm);
+
+    scrollArea_->setWidget(scrollAreaContents_);
+    topLayout_->addLayout(topBarLayout_);
+    topLayout_->addWidget(scrollArea_, Qt::AlignTop);
+    topLayout_->addStretch(1);
+    topLayout_->addWidget(versionInfo);
+
+    connect(themeCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &text) {
+                settings_->setTheme(text.toLower());
+                emit themeChanged();
+            });
+    connect(scaleFactorCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); });
+    connect(fontSizeCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); });
+    connect(fontSelectionCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &family) { settings_->setFontFamily(family.trimmed()); });
+    connect(emojiFontSelectionCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); });
+
+    connect(ringtoneCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &ringtone) {
+                if (ringtone == "Other...") {
+                    QString homeFolder =
+                      QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
+                    auto filepath = QFileDialog::getOpenFileName(
+                      this, tr("Select a file"), homeFolder, tr("All Files (*)"));
+                    if (!filepath.isEmpty()) {
+                        const auto &oldSetting = settings_->ringtone();
+                        if (oldSetting != "Mute" && oldSetting != "Default")
+                            ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting));
+                        settings_->setRingtone(filepath);
+                        ringtoneCombo_->addItem(filepath);
+                        ringtoneCombo_->setCurrentText(filepath);
+                    } else {
+                        ringtoneCombo_->setCurrentText(settings_->ringtone());
+                    }
+                } else if (ringtone == "Mute" || ringtone == "Default") {
+                    const auto &oldSetting = settings_->ringtone();
+                    if (oldSetting != "Mute" && oldSetting != "Default")
+                        ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting));
+                    settings_->setRingtone(ringtone);
                 }
-                emit trayOptionChanged(enabled);
-        });
+            });
+
+    connect(microphoneCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &microphone) { settings_->setMicrophone(microphone); });
+
+    connect(cameraCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &camera) {
+                settings_->setCamera(camera);
+                std::vector<std::string> resolutions =
+                  CallDevices::instance().resolutions(camera.toStdString());
+                cameraResolutionCombo_->clear();
+                for (const auto &resolution : resolutions)
+                    cameraResolutionCombo_->addItem(QString::fromStdString(resolution));
+            });
+
+    connect(cameraResolutionCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &resolution) {
+                settings_->setCameraResolution(resolution);
+                std::vector<std::string> frameRates = CallDevices::instance().frameRates(
+                  settings_->camera().toStdString(), resolution.toStdString());
+                cameraFrameRateCombo_->clear();
+                for (const auto &frameRate : frameRates)
+                    cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate));
+            });
+
+    connect(cameraFrameRateCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); });
+
+    connect(trayToggle_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setTray(enabled);
+        if (enabled) {
+            startInTrayToggle_->setChecked(false);
+            startInTrayToggle_->setEnabled(true);
+            startInTrayToggle_->setState(false);
+            settings_->setStartInTray(false);
+        } else {
+            startInTrayToggle_->setChecked(false);
+            startInTrayToggle_->setState(false);
+            startInTrayToggle_->setDisabled(true);
+            settings_->setStartInTray(false);
+        }
+        emit trayOptionChanged(enabled);
+    });
 
-        connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setStartInTray(enabled);
-        });
+    connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setStartInTray(enabled);
+    });
 
-        connect(mobileMode_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setMobileMode(enabled);
-        });
+    connect(mobileMode_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setMobileMode(enabled);
+    });
 
-        connect(groupViewToggle_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setGroupView(enabled);
-        });
+    connect(groupViewToggle_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setGroupView(enabled);
+    });
 
-        connect(decryptSidebar_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setDecryptSidebar(enabled);
-        });
+    connect(decryptSidebar_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setDecryptSidebar(enabled);
+    });
 
-        connect(privacyScreen_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setPrivacyScreen(enabled);
-                if (enabled) {
-                        privacyScreenTimeout_->setEnabled(true);
-                } else {
-                        privacyScreenTimeout_->setDisabled(true);
-                }
-        });
+    connect(privacyScreen_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setPrivacyScreen(enabled);
+        if (enabled) {
+            privacyScreenTimeout_->setEnabled(true);
+        } else {
+            privacyScreenTimeout_->setDisabled(true);
+        }
+    });
+
+    connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setOnlyShareKeysWithVerifiedUsers(enabled);
+    });
+
+    connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setShareKeysWithTrustedUsers(enabled);
+    });
+
+    connect(useOnlineKeyBackup_, &Toggle::toggled, this, [this](bool enabled) {
+        if (enabled) {
+            if (QMessageBox::question(
+                  this,
+                  tr("Enable online key backup"),
+                  tr("The Nheko authors recommend not enabling online key backup until "
+                     "symmetric online key backup is available. Enable anyway?")) !=
+                QMessageBox::StandardButton::Yes) {
+                useOnlineKeyBackup_->setState(false);
+                return;
+            }
+        }
+        settings_->setUseOnlineKeyBackup(enabled);
+    });
 
-        connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setOnlyShareKeysWithVerifiedUsers(enabled);
-        });
+    connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setAvatarCircles(enabled);
+    });
 
-        connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setShareKeysWithTrustedUsers(enabled);
+    if (JdenticonProvider::isAvailable())
+        connect(useIdenticon_, &Toggle::toggled, this, [this](bool enabled) {
+            settings_->setUseIdenticon(enabled);
         });
+    else
+        useIdenticon_->setDisabled(true);
 
-        connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setAvatarCircles(enabled);
-        });
+    connect(
+      markdown_, &Toggle::toggled, this, [this](bool enabled) { settings_->setMarkdown(enabled); });
 
-        connect(markdown_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setMarkdown(enabled);
-        });
+    connect(animateImagesOnHover_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setAnimateImagesOnHover(enabled);
+    });
 
-        connect(typingNotifications_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setTypingNotifications(enabled);
-        });
+    connect(typingNotifications_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setTypingNotifications(enabled);
+    });
 
-        connect(sortByImportance_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setSortByImportance(enabled);
-        });
+    connect(sortByImportance_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setSortByImportance(enabled);
+    });
 
-        connect(timelineButtonsToggle_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setButtonsInTimeline(enabled);
-        });
+    connect(timelineButtonsToggle_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setButtonsInTimeline(enabled);
+    });
 
-        connect(readReceipts_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setReadReceipts(enabled);
-        });
+    connect(readReceipts_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setReadReceipts(enabled);
+    });
 
-        connect(desktopNotifications_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setDesktopNotifications(enabled);
-        });
+    connect(desktopNotifications_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setDesktopNotifications(enabled);
+    });
 
-        connect(alertOnNotification_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setAlertOnNotification(enabled);
-        });
+    connect(alertOnNotification_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setAlertOnNotification(enabled);
+    });
 
-        connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setMessageHoverHighlight(enabled);
-        });
+    connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setMessageHoverHighlight(enabled);
+    });
 
-        connect(enlargeEmojiOnlyMessages_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setEnlargeEmojiOnlyMessages(enabled);
-        });
+    connect(enlargeEmojiOnlyMessages_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setEnlargeEmojiOnlyMessages(enabled);
+    });
 
-        connect(useStunServer_, &Toggle::toggled, this, [this](bool enabled) {
-                settings_->setUseStunServer(enabled);
-        });
+    connect(useStunServer_, &Toggle::toggled, this, [this](bool enabled) {
+        settings_->setUseStunServer(enabled);
+    });
 
-        connect(timelineMaxWidthSpin_,
-                qOverload<int>(&QSpinBox::valueChanged),
-                this,
-                [this](int newValue) { settings_->setTimelineMaxWidth(newValue); });
+    connect(timelineMaxWidthSpin_,
+            qOverload<int>(&QSpinBox::valueChanged),
+            this,
+            [this](int newValue) { settings_->setTimelineMaxWidth(newValue); });
 
-        connect(privacyScreenTimeout_,
-                qOverload<int>(&QSpinBox::valueChanged),
-                this,
-                [this](int newValue) { settings_->setPrivacyScreenTimeout(newValue); });
+    connect(privacyScreenTimeout_,
+            qOverload<int>(&QSpinBox::valueChanged),
+            this,
+            [this](int newValue) { settings_->setPrivacyScreenTimeout(newValue); });
 
-        connect(
-          sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys);
+    connect(
+      sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys);
 
-        connect(
-          sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys);
+    connect(
+      sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys);
 
-        connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() {
-                olm::request_cross_signing_keys();
-        });
+    connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() {
+        olm::request_cross_signing_keys();
+    });
 
-        connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() {
-                olm::download_cross_signing_keys();
-        });
+    connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() {
+        olm::download_cross_signing_keys();
+    });
 
-        connect(backBtn_, &QPushButton::clicked, this, [this]() {
-                settings_->save();
-                emit moveBack();
-        });
+    connect(backBtn_, &QPushButton::clicked, this, [this]() {
+        settings_->save();
+        emit moveBack();
+    });
 }
 
 void
 UserSettingsPage::showEvent(QShowEvent *)
 {
-        // FIXME macOS doesn't show the full option unless a space is added.
-        utils::restoreCombobox(fontSizeCombo_, QString::number(settings_->fontSize()) + " ");
-        utils::restoreCombobox(scaleFactorCombo_, QString::number(utils::scaleFactor()));
-        utils::restoreCombobox(themeCombo_, settings_->theme());
-        utils::restoreCombobox(ringtoneCombo_, settings_->ringtone());
-
-        trayToggle_->setState(settings_->tray());
-        startInTrayToggle_->setState(settings_->startInTray());
-        groupViewToggle_->setState(settings_->groupView());
-        decryptSidebar_->setState(settings_->decryptSidebar());
-        privacyScreen_->setState(settings_->privacyScreen());
-        onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers());
-        shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
-        avatarCircles_->setState(settings_->avatarCircles());
-        typingNotifications_->setState(settings_->typingNotifications());
-        sortByImportance_->setState(settings_->sortByImportance());
-        timelineButtonsToggle_->setState(settings_->buttonsInTimeline());
-        mobileMode_->setState(settings_->mobileMode());
-        readReceipts_->setState(settings_->readReceipts());
-        markdown_->setState(settings_->markdown());
-        desktopNotifications_->setState(settings_->hasDesktopNotifications());
-        alertOnNotification_->setState(settings_->hasAlertOnNotification());
-        messageHoverHighlight_->setState(settings_->messageHoverHighlight());
-        enlargeEmojiOnlyMessages_->setState(settings_->enlargeEmojiOnlyMessages());
-        deviceIdValue_->setText(QString::fromStdString(http::client()->device_id()));
-        timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth());
-        privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout());
-
-        auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString());
-        microphoneCombo_->clear();
-        for (const auto &m : mics)
-                microphoneCombo_->addItem(QString::fromStdString(m));
-
-        auto cameraResolution = settings_->cameraResolution();
-        auto cameraFrameRate  = settings_->cameraFrameRate();
-
-        auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString());
-        cameraCombo_->clear();
-        for (const auto &c : cameras)
-                cameraCombo_->addItem(QString::fromStdString(c));
-
-        utils::restoreCombobox(cameraResolutionCombo_, cameraResolution);
-        utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate);
-
-        useStunServer_->setState(settings_->useStunServer());
-
-        deviceFingerprintValue_->setText(
-          utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519));
+    // FIXME macOS doesn't show the full option unless a space is added.
+    utils::restoreCombobox(fontSizeCombo_, QString::number(settings_->fontSize()) + " ");
+    utils::restoreCombobox(scaleFactorCombo_, QString::number(utils::scaleFactor()));
+    utils::restoreCombobox(themeCombo_, settings_->theme());
+    utils::restoreCombobox(ringtoneCombo_, settings_->ringtone());
+
+    trayToggle_->setState(settings_->tray());
+    startInTrayToggle_->setState(settings_->startInTray());
+    groupViewToggle_->setState(settings_->groupView());
+    decryptSidebar_->setState(settings_->decryptSidebar());
+    privacyScreen_->setState(settings_->privacyScreen());
+    onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers());
+    shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
+    useOnlineKeyBackup_->setState(settings_->useOnlineKeyBackup());
+    avatarCircles_->setState(settings_->avatarCircles());
+    typingNotifications_->setState(settings_->typingNotifications());
+    sortByImportance_->setState(settings_->sortByImportance());
+    timelineButtonsToggle_->setState(settings_->buttonsInTimeline());
+    mobileMode_->setState(settings_->mobileMode());
+    readReceipts_->setState(settings_->readReceipts());
+    markdown_->setState(settings_->markdown());
+    desktopNotifications_->setState(settings_->hasDesktopNotifications());
+    alertOnNotification_->setState(settings_->hasAlertOnNotification());
+    messageHoverHighlight_->setState(settings_->messageHoverHighlight());
+    enlargeEmojiOnlyMessages_->setState(settings_->enlargeEmojiOnlyMessages());
+    deviceIdValue_->setText(QString::fromStdString(http::client()->device_id()));
+    timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth());
+    privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout());
+
+    auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString());
+    microphoneCombo_->clear();
+    for (const auto &m : mics)
+        microphoneCombo_->addItem(QString::fromStdString(m));
+
+    auto cameraResolution = settings_->cameraResolution();
+    auto cameraFrameRate  = settings_->cameraFrameRate();
+
+    auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString());
+    cameraCombo_->clear();
+    for (const auto &c : cameras)
+        cameraCombo_->addItem(QString::fromStdString(c));
+
+    utils::restoreCombobox(cameraResolutionCombo_, cameraResolution);
+    utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate);
+
+    useStunServer_->setState(settings_->useStunServer());
+
+    deviceFingerprintValue_->setText(
+      utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519));
 }
 
 void
 UserSettingsPage::paintEvent(QPaintEvent *)
 {
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+    QStyleOption opt;
+    opt.init(this);
+    QPainter p(this);
+    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
 }
 
 void
 UserSettingsPage::importSessionKeys()
 {
-        const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
-        const QString fileName =
-          QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, "");
-
-        QFile file(fileName);
-        if (!file.open(QIODevice::ReadOnly)) {
-                QMessageBox::warning(this, tr("Error"), file.errorString());
-                return;
-        }
-
-        auto bin     = file.peek(file.size());
-        auto payload = std::string(bin.data(), bin.size());
-
-        bool ok;
-        auto password = QInputDialog::getText(this,
-                                              tr("File Password"),
-                                              tr("Enter the passphrase to decrypt the file:"),
-                                              QLineEdit::Password,
-                                              "",
-                                              &ok);
-        if (!ok)
-                return;
-
-        if (password.isEmpty()) {
-                QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty"));
-                return;
-        }
-
-        try {
-                auto sessions =
-                  mtx::crypto::decrypt_exported_sessions(payload, password.toStdString());
-                cache::importSessionKeys(std::move(sessions));
-        } catch (const std::exception &e) {
-                QMessageBox::warning(this, tr("Error"), e.what());
-        }
+    const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
+    const QString fileName =
+      QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, "");
+
+    QFile file(fileName);
+    if (!file.open(QIODevice::ReadOnly)) {
+        QMessageBox::warning(this, tr("Error"), file.errorString());
+        return;
+    }
+
+    auto bin     = file.peek(file.size());
+    auto payload = std::string(bin.data(), bin.size());
+
+    bool ok;
+    auto password = QInputDialog::getText(this,
+                                          tr("File Password"),
+                                          tr("Enter the passphrase to decrypt the file:"),
+                                          QLineEdit::Password,
+                                          "",
+                                          &ok);
+    if (!ok)
+        return;
+
+    if (password.isEmpty()) {
+        QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty"));
+        return;
+    }
+
+    try {
+        auto sessions = mtx::crypto::decrypt_exported_sessions(payload, password.toStdString());
+        cache::importSessionKeys(std::move(sessions));
+    } catch (const std::exception &e) {
+        QMessageBox::warning(this, tr("Error"), e.what());
+    }
 }
 
 void
 UserSettingsPage::exportSessionKeys()
 {
-        // Open password dialog.
-        bool ok;
-        auto password = QInputDialog::getText(this,
-                                              tr("File Password"),
-                                              tr("Enter passphrase to encrypt your session keys:"),
-                                              QLineEdit::Password,
-                                              "",
-                                              &ok);
-        if (!ok)
-                return;
-
-        if (password.isEmpty()) {
-                QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty"));
-                return;
-        }
-
-        // Open file dialog to save the file.
-        const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
-        const QString fileName =
-          QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", "");
-
-        QFile file(fileName);
-        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
-                QMessageBox::warning(this, tr("Error"), file.errorString());
-                return;
-        }
-
-        // Export sessions & save to file.
-        try {
-                auto encrypted_blob = mtx::crypto::encrypt_exported_sessions(
-                  cache::exportSessionKeys(), password.toStdString());
-
-                QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob));
-
-                QString prefix("-----BEGIN MEGOLM SESSION DATA-----");
-                QString suffix("-----END MEGOLM SESSION DATA-----");
-                QString newline("\n");
-                QTextStream out(&file);
-                out << prefix << newline << b64 << newline << suffix << newline;
-                file.close();
-        } catch (const std::exception &e) {
-                QMessageBox::warning(this, tr("Error"), e.what());
-        }
+    // Open password dialog.
+    bool ok;
+    auto password = QInputDialog::getText(this,
+                                          tr("File Password"),
+                                          tr("Enter passphrase to encrypt your session keys:"),
+                                          QLineEdit::Password,
+                                          "",
+                                          &ok);
+    if (!ok)
+        return;
+
+    if (password.isEmpty()) {
+        QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty"));
+        return;
+    }
+
+    // Open file dialog to save the file.
+    const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
+    const QString fileName =
+      QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", "");
+
+    QFile file(fileName);
+    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+        QMessageBox::warning(this, tr("Error"), file.errorString());
+        return;
+    }
+
+    // Export sessions & save to file.
+    try {
+        auto encrypted_blob = mtx::crypto::encrypt_exported_sessions(cache::exportSessionKeys(),
+                                                                     password.toStdString());
+
+        QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob));
+
+        QString prefix("-----BEGIN MEGOLM SESSION DATA-----");
+        QString suffix("-----END MEGOLM SESSION DATA-----");
+        QString newline("\n");
+        QTextStream out(&file);
+        out << prefix << newline << b64 << newline << suffix << newline;
+        file.close();
+    } catch (const std::exception &e) {
+        QMessageBox::warning(this, tr("Error"), e.what());
+    }
 }
 
 void
 UserSettingsPage::updateSecretStatus()
 {
-        QString ok      = "QLabel { color : #00cc66; }";
-        QString notSoOk = "QLabel { color : #ff9933; }";
-
-        auto updateLabel = [&ok, &notSoOk](QLabel *label, const std::string &secretName) {
-                if (cache::secret(secretName)) {
-                        label->setStyleSheet(ok);
-                        label->setText(tr("CACHED"));
-                } else {
-                        if (secretName == mtx::secret_storage::secrets::cross_signing_master)
-                                label->setStyleSheet(ok);
-                        else
-                                label->setStyleSheet(notSoOk);
-                        label->setText(tr("NOT CACHED"));
-                }
-        };
-
-        updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master);
-        updateLabel(userSigningSecretCached,
-                    mtx::secret_storage::secrets::cross_signing_user_signing);
-        updateLabel(selfSigningSecretCached,
-                    mtx::secret_storage::secrets::cross_signing_self_signing);
-        updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1);
+    QString ok      = "QLabel { color : #00cc66; }";
+    QString notSoOk = "QLabel { color : #ff9933; }";
+
+    auto updateLabel = [&ok, &notSoOk](QLabel *label, const std::string &secretName) {
+        if (cache::secret(secretName)) {
+            label->setStyleSheet(ok);
+            label->setText(tr("CACHED"));
+        } else {
+            if (secretName == mtx::secret_storage::secrets::cross_signing_master)
+                label->setStyleSheet(ok);
+            else
+                label->setStyleSheet(notSoOk);
+            label->setText(tr("NOT CACHED"));
+        }
+    };
+
+    updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master);
+    updateLabel(userSigningSecretCached, mtx::secret_storage::secrets::cross_signing_user_signing);
+    updateLabel(selfSigningSecretCached, mtx::secret_storage::secrets::cross_signing_self_signing);
+    updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1);
 }
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 84940e47077fa4c1c9986f1a10038b7c33e78fe3..31e28db25f68eaef3156db29f0851821fd362dfa 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -12,6 +12,7 @@
 #include <QSharedPointer>
 #include <QWidget>
 
+#include "JdenticonProvider.h"
 #include <optional>
 
 class Toggle;
@@ -29,381 +30,395 @@ constexpr int LayoutBottomMargin = LayoutTopMargin;
 
 class UserSettings : public QObject
 {
-        Q_OBJECT
-
-        Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)
-        Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE
-                     setMessageHoverHighlight NOTIFY messageHoverHighlightChanged)
-        Q_PROPERTY(bool enlargeEmojiOnlyMessages READ enlargeEmojiOnlyMessages WRITE
-                     setEnlargeEmojiOnlyMessages NOTIFY enlargeEmojiOnlyMessagesChanged)
-        Q_PROPERTY(bool tray READ tray WRITE setTray NOTIFY trayChanged)
-        Q_PROPERTY(bool startInTray READ startInTray WRITE setStartInTray NOTIFY startInTrayChanged)
-        Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged)
-        Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
-        Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications
-                     NOTIFY typingNotificationsChanged)
-        Q_PROPERTY(bool sortByImportance READ sortByImportance WRITE setSortByImportance NOTIFY
-                     roomSortingChanged)
-        Q_PROPERTY(bool buttonsInTimeline READ buttonsInTimeline WRITE setButtonsInTimeline NOTIFY
-                     buttonInTimelineChanged)
-        Q_PROPERTY(
-          bool readReceipts READ readReceipts WRITE setReadReceipts NOTIFY readReceiptsChanged)
-        Q_PROPERTY(bool desktopNotifications READ hasDesktopNotifications WRITE
-                     setDesktopNotifications NOTIFY desktopNotificationsChanged)
-        Q_PROPERTY(bool alertOnNotification READ hasAlertOnNotification WRITE setAlertOnNotification
-                     NOTIFY alertOnNotificationChanged)
-        Q_PROPERTY(
-          bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged)
-        Q_PROPERTY(bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY
-                     decryptSidebarChanged)
-        Q_PROPERTY(
-          bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged)
-        Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout
-                     NOTIFY privacyScreenTimeoutChanged)
-        Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY
-                     timelineMaxWidthChanged)
-        Q_PROPERTY(
-          int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged)
-        Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY
-                     communityListWidthChanged)
-        Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged)
-        Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged)
-        Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged)
-        Q_PROPERTY(
-          QString emojiFont READ emojiFont WRITE setEmojiFontFamily NOTIFY emojiFontChanged)
-        Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged)
-        Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged)
-        Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged)
-        Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged)
-        Q_PROPERTY(QString cameraResolution READ cameraResolution WRITE setCameraResolution NOTIFY
-                     cameraResolutionChanged)
-        Q_PROPERTY(QString cameraFrameRate READ cameraFrameRate WRITE setCameraFrameRate NOTIFY
-                     cameraFrameRateChanged)
-        Q_PROPERTY(int screenShareFrameRate READ screenShareFrameRate WRITE setScreenShareFrameRate
-                     NOTIFY screenShareFrameRateChanged)
-        Q_PROPERTY(bool screenSharePiP READ screenSharePiP WRITE setScreenSharePiP NOTIFY
-                     screenSharePiPChanged)
-        Q_PROPERTY(bool screenShareRemoteVideo READ screenShareRemoteVideo WRITE
-                     setScreenShareRemoteVideo NOTIFY screenShareRemoteVideoChanged)
-        Q_PROPERTY(bool screenShareHideCursor READ screenShareHideCursor WRITE
-                     setScreenShareHideCursor NOTIFY screenShareHideCursorChanged)
-        Q_PROPERTY(
-          bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
-        Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE
-                     setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged)
-        Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
-                     setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
-        Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
-        Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged)
-        Q_PROPERTY(
-          QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged)
-        Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged)
-        Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
-        Q_PROPERTY(bool disableCertificateValidation READ disableCertificateValidation WRITE
-                     setDisableCertificateValidation NOTIFY disableCertificateValidationChanged)
-
-        UserSettings();
+    Q_OBJECT
+
+    Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)
+    Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE setMessageHoverHighlight
+                 NOTIFY messageHoverHighlightChanged)
+    Q_PROPERTY(bool enlargeEmojiOnlyMessages READ enlargeEmojiOnlyMessages WRITE
+                 setEnlargeEmojiOnlyMessages NOTIFY enlargeEmojiOnlyMessagesChanged)
+    Q_PROPERTY(bool tray READ tray WRITE setTray NOTIFY trayChanged)
+    Q_PROPERTY(bool startInTray READ startInTray WRITE setStartInTray NOTIFY startInTrayChanged)
+    Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged)
+    Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
+    Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
+                 NOTIFY animateImagesOnHoverChanged)
+    Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications NOTIFY
+                 typingNotificationsChanged)
+    Q_PROPERTY(bool sortByImportance READ sortByImportance WRITE setSortByImportance NOTIFY
+                 roomSortingChanged)
+    Q_PROPERTY(bool buttonsInTimeline READ buttonsInTimeline WRITE setButtonsInTimeline NOTIFY
+                 buttonInTimelineChanged)
+    Q_PROPERTY(bool readReceipts READ readReceipts WRITE setReadReceipts NOTIFY readReceiptsChanged)
+    Q_PROPERTY(bool desktopNotifications READ hasDesktopNotifications WRITE setDesktopNotifications
+                 NOTIFY desktopNotificationsChanged)
+    Q_PROPERTY(bool alertOnNotification READ hasAlertOnNotification WRITE setAlertOnNotification
+                 NOTIFY alertOnNotificationChanged)
+    Q_PROPERTY(
+      bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged)
+    Q_PROPERTY(
+      bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY decryptSidebarChanged)
+    Q_PROPERTY(
+      bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged)
+    Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout
+                 NOTIFY privacyScreenTimeoutChanged)
+    Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY
+                 timelineMaxWidthChanged)
+    Q_PROPERTY(
+      int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged)
+    Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY
+                 communityListWidthChanged)
+    Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged)
+    Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged)
+    Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged)
+    Q_PROPERTY(QString emojiFont READ emojiFont WRITE setEmojiFontFamily NOTIFY emojiFontChanged)
+    Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged)
+    Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged)
+    Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged)
+    Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged)
+    Q_PROPERTY(QString cameraResolution READ cameraResolution WRITE setCameraResolution NOTIFY
+                 cameraResolutionChanged)
+    Q_PROPERTY(QString cameraFrameRate READ cameraFrameRate WRITE setCameraFrameRate NOTIFY
+                 cameraFrameRateChanged)
+    Q_PROPERTY(int screenShareFrameRate READ screenShareFrameRate WRITE setScreenShareFrameRate
+                 NOTIFY screenShareFrameRateChanged)
+    Q_PROPERTY(
+      bool screenSharePiP READ screenSharePiP WRITE setScreenSharePiP NOTIFY screenSharePiPChanged)
+    Q_PROPERTY(bool screenShareRemoteVideo READ screenShareRemoteVideo WRITE
+                 setScreenShareRemoteVideo NOTIFY screenShareRemoteVideoChanged)
+    Q_PROPERTY(bool screenShareHideCursor READ screenShareHideCursor WRITE setScreenShareHideCursor
+                 NOTIFY screenShareHideCursorChanged)
+    Q_PROPERTY(
+      bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
+    Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE
+                 setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged)
+    Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
+                 setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
+    Q_PROPERTY(bool useOnlineKeyBackup READ useOnlineKeyBackup WRITE setUseOnlineKeyBackup NOTIFY
+                 useOnlineKeyBackupChanged)
+    Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
+    Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged)
+    Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged)
+    Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged)
+    Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
+    Q_PROPERTY(bool disableCertificateValidation READ disableCertificateValidation WRITE
+                 setDisableCertificateValidation NOTIFY disableCertificateValidationChanged)
+    Q_PROPERTY(bool useIdenticon READ useIdenticon WRITE setUseIdenticon NOTIFY useIdenticonChanged)
+
+    UserSettings();
 
 public:
-        static QSharedPointer<UserSettings> instance();
-        static void initialize(std::optional<QString> profile);
-
-        QSettings *qsettings() { return &settings; }
-
-        enum class Presence
-        {
-                AutomaticPresence,
-                Online,
-                Unavailable,
-                Offline,
-        };
-        Q_ENUM(Presence)
-
-        void save();
-        void load(std::optional<QString> profile);
-        void applyTheme();
-        void setTheme(QString theme);
-        void setMessageHoverHighlight(bool state);
-        void setEnlargeEmojiOnlyMessages(bool state);
-        void setTray(bool state);
-        void setStartInTray(bool state);
-        void setMobileMode(bool mode);
-        void setFontSize(double size);
-        void setFontFamily(QString family);
-        void setEmojiFontFamily(QString family);
-        void setGroupView(bool state);
-        void setMarkdown(bool state);
-        void setReadReceipts(bool state);
-        void setTypingNotifications(bool state);
-        void setSortByImportance(bool state);
-        void setButtonsInTimeline(bool state);
-        void setTimelineMaxWidth(int state);
-        void setCommunityListWidth(int state);
-        void setRoomListWidth(int state);
-        void setDesktopNotifications(bool state);
-        void setAlertOnNotification(bool state);
-        void setAvatarCircles(bool state);
-        void setDecryptSidebar(bool state);
-        void setPrivacyScreen(bool state);
-        void setPrivacyScreenTimeout(int state);
-        void setPresence(Presence state);
-        void setRingtone(QString ringtone);
-        void setMicrophone(QString microphone);
-        void setCamera(QString camera);
-        void setCameraResolution(QString resolution);
-        void setCameraFrameRate(QString frameRate);
-        void setScreenShareFrameRate(int frameRate);
-        void setScreenSharePiP(bool state);
-        void setScreenShareRemoteVideo(bool state);
-        void setScreenShareHideCursor(bool state);
-        void setUseStunServer(bool state);
-        void setOnlyShareKeysWithVerifiedUsers(bool state);
-        void setShareKeysWithTrustedUsers(bool state);
-        void setProfile(QString profile);
-        void setUserId(QString userId);
-        void setAccessToken(QString accessToken);
-        void setDeviceId(QString deviceId);
-        void setHomeserver(QString homeserver);
-        void setDisableCertificateValidation(bool disabled);
-        void setHiddenTags(QStringList hiddenTags);
-
-        QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
-        bool messageHoverHighlight() const { return messageHoverHighlight_; }
-        bool enlargeEmojiOnlyMessages() const { return enlargeEmojiOnlyMessages_; }
-        bool tray() const { return tray_; }
-        bool startInTray() const { return startInTray_; }
-        bool groupView() const { return groupView_; }
-        bool avatarCircles() const { return avatarCircles_; }
-        bool decryptSidebar() const { return decryptSidebar_; }
-        bool privacyScreen() const { return privacyScreen_; }
-        int privacyScreenTimeout() const { return privacyScreenTimeout_; }
-        bool markdown() const { return markdown_; }
-        bool typingNotifications() const { return typingNotifications_; }
-        bool sortByImportance() const { return sortByImportance_; }
-        bool buttonsInTimeline() const { return buttonsInTimeline_; }
-        bool mobileMode() const { return mobileMode_; }
-        bool readReceipts() const { return readReceipts_; }
-        bool hasDesktopNotifications() const { return hasDesktopNotifications_; }
-        bool hasAlertOnNotification() const { return hasAlertOnNotification_; }
-        bool hasNotifications() const
-        {
-                return hasDesktopNotifications() || hasAlertOnNotification();
+    static QSharedPointer<UserSettings> instance();
+    static void initialize(std::optional<QString> profile);
+
+    QSettings *qsettings() { return &settings; }
+
+    enum class Presence
+    {
+        AutomaticPresence,
+        Online,
+        Unavailable,
+        Offline,
+    };
+    Q_ENUM(Presence)
+
+    void save();
+    void load(std::optional<QString> profile);
+    void applyTheme();
+    void setTheme(QString theme);
+    void setMessageHoverHighlight(bool state);
+    void setEnlargeEmojiOnlyMessages(bool state);
+    void setTray(bool state);
+    void setStartInTray(bool state);
+    void setMobileMode(bool mode);
+    void setFontSize(double size);
+    void setFontFamily(QString family);
+    void setEmojiFontFamily(QString family);
+    void setGroupView(bool state);
+    void setMarkdown(bool state);
+    void setAnimateImagesOnHover(bool state);
+    void setReadReceipts(bool state);
+    void setTypingNotifications(bool state);
+    void setSortByImportance(bool state);
+    void setButtonsInTimeline(bool state);
+    void setTimelineMaxWidth(int state);
+    void setCommunityListWidth(int state);
+    void setRoomListWidth(int state);
+    void setDesktopNotifications(bool state);
+    void setAlertOnNotification(bool state);
+    void setAvatarCircles(bool state);
+    void setDecryptSidebar(bool state);
+    void setPrivacyScreen(bool state);
+    void setPrivacyScreenTimeout(int state);
+    void setPresence(Presence state);
+    void setRingtone(QString ringtone);
+    void setMicrophone(QString microphone);
+    void setCamera(QString camera);
+    void setCameraResolution(QString resolution);
+    void setCameraFrameRate(QString frameRate);
+    void setScreenShareFrameRate(int frameRate);
+    void setScreenSharePiP(bool state);
+    void setScreenShareRemoteVideo(bool state);
+    void setScreenShareHideCursor(bool state);
+    void setUseStunServer(bool state);
+    void setOnlyShareKeysWithVerifiedUsers(bool state);
+    void setShareKeysWithTrustedUsers(bool state);
+    void setUseOnlineKeyBackup(bool state);
+    void setProfile(QString profile);
+    void setUserId(QString userId);
+    void setAccessToken(QString accessToken);
+    void setDeviceId(QString deviceId);
+    void setHomeserver(QString homeserver);
+    void setDisableCertificateValidation(bool disabled);
+    void setHiddenTags(QStringList hiddenTags);
+    void setUseIdenticon(bool state);
+
+    QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
+    bool messageHoverHighlight() const { return messageHoverHighlight_; }
+    bool enlargeEmojiOnlyMessages() const { return enlargeEmojiOnlyMessages_; }
+    bool tray() const { return tray_; }
+    bool startInTray() const { return startInTray_; }
+    bool groupView() const { return groupView_; }
+    bool avatarCircles() const { return avatarCircles_; }
+    bool decryptSidebar() const { return decryptSidebar_; }
+    bool privacyScreen() const { return privacyScreen_; }
+    int privacyScreenTimeout() const { return privacyScreenTimeout_; }
+    bool markdown() const { return markdown_; }
+    bool animateImagesOnHover() const { return animateImagesOnHover_; }
+    bool typingNotifications() const { return typingNotifications_; }
+    bool sortByImportance() const { return sortByImportance_; }
+    bool buttonsInTimeline() const { return buttonsInTimeline_; }
+    bool mobileMode() const { return mobileMode_; }
+    bool readReceipts() const { return readReceipts_; }
+    bool hasDesktopNotifications() const { return hasDesktopNotifications_; }
+    bool hasAlertOnNotification() const { return hasAlertOnNotification_; }
+    bool hasNotifications() const { return hasDesktopNotifications() || hasAlertOnNotification(); }
+    int timelineMaxWidth() const { return timelineMaxWidth_; }
+    int communityListWidth() const { return communityListWidth_; }
+    int roomListWidth() const { return roomListWidth_; }
+    double fontSize() const { return baseFontSize_; }
+    QString font() const { return font_; }
+    QString emojiFont() const
+    {
+        if (emojiFont_ == "Default") {
+            return tr("Default");
         }
-        int timelineMaxWidth() const { return timelineMaxWidth_; }
-        int communityListWidth() const { return communityListWidth_; }
-        int roomListWidth() const { return roomListWidth_; }
-        double fontSize() const { return baseFontSize_; }
-        QString font() const { return font_; }
-        QString emojiFont() const
-        {
-                if (emojiFont_ == "Default") {
-                        return tr("Default");
-                }
-
-                return emojiFont_;
-        }
-        Presence presence() const { return presence_; }
-        QString ringtone() const { return ringtone_; }
-        QString microphone() const { return microphone_; }
-        QString camera() const { return camera_; }
-        QString cameraResolution() const { return cameraResolution_; }
-        QString cameraFrameRate() const { return cameraFrameRate_; }
-        int screenShareFrameRate() const { return screenShareFrameRate_; }
-        bool screenSharePiP() const { return screenSharePiP_; }
-        bool screenShareRemoteVideo() const { return screenShareRemoteVideo_; }
-        bool screenShareHideCursor() const { return screenShareHideCursor_; }
-        bool useStunServer() const { return useStunServer_; }
-        bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
-        bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; }
-        QString profile() const { return profile_; }
-        QString userId() const { return userId_; }
-        QString accessToken() const { return accessToken_; }
-        QString deviceId() const { return deviceId_; }
-        QString homeserver() const { return homeserver_; }
-        bool disableCertificateValidation() const { return disableCertificateValidation_; }
-        QStringList hiddenTags() const { return hiddenTags_; }
+
+        return emojiFont_;
+    }
+    Presence presence() const { return presence_; }
+    QString ringtone() const { return ringtone_; }
+    QString microphone() const { return microphone_; }
+    QString camera() const { return camera_; }
+    QString cameraResolution() const { return cameraResolution_; }
+    QString cameraFrameRate() const { return cameraFrameRate_; }
+    int screenShareFrameRate() const { return screenShareFrameRate_; }
+    bool screenSharePiP() const { return screenSharePiP_; }
+    bool screenShareRemoteVideo() const { return screenShareRemoteVideo_; }
+    bool screenShareHideCursor() const { return screenShareHideCursor_; }
+    bool useStunServer() const { return useStunServer_; }
+    bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
+    bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; }
+    bool useOnlineKeyBackup() const { return useOnlineKeyBackup_; }
+    QString profile() const { return profile_; }
+    QString userId() const { return userId_; }
+    QString accessToken() const { return accessToken_; }
+    QString deviceId() const { return deviceId_; }
+    QString homeserver() const { return homeserver_; }
+    bool disableCertificateValidation() const { return disableCertificateValidation_; }
+    QStringList hiddenTags() const { return hiddenTags_; }
+    bool useIdenticon() const { return useIdenticon_ && JdenticonProvider::isAvailable(); }
 
 signals:
-        void groupViewStateChanged(bool state);
-        void roomSortingChanged(bool state);
-        void themeChanged(QString state);
-        void messageHoverHighlightChanged(bool state);
-        void enlargeEmojiOnlyMessagesChanged(bool state);
-        void trayChanged(bool state);
-        void startInTrayChanged(bool state);
-        void markdownChanged(bool state);
-        void typingNotificationsChanged(bool state);
-        void buttonInTimelineChanged(bool state);
-        void readReceiptsChanged(bool state);
-        void desktopNotificationsChanged(bool state);
-        void alertOnNotificationChanged(bool state);
-        void avatarCirclesChanged(bool state);
-        void decryptSidebarChanged(bool state);
-        void privacyScreenChanged(bool state);
-        void privacyScreenTimeoutChanged(int state);
-        void timelineMaxWidthChanged(int state);
-        void roomListWidthChanged(int state);
-        void communityListWidthChanged(int state);
-        void mobileModeChanged(bool mode);
-        void fontSizeChanged(double state);
-        void fontChanged(QString state);
-        void emojiFontChanged(QString state);
-        void presenceChanged(Presence state);
-        void ringtoneChanged(QString ringtone);
-        void microphoneChanged(QString microphone);
-        void cameraChanged(QString camera);
-        void cameraResolutionChanged(QString resolution);
-        void cameraFrameRateChanged(QString frameRate);
-        void screenShareFrameRateChanged(int frameRate);
-        void screenSharePiPChanged(bool state);
-        void screenShareRemoteVideoChanged(bool state);
-        void screenShareHideCursorChanged(bool state);
-        void useStunServerChanged(bool state);
-        void onlyShareKeysWithVerifiedUsersChanged(bool state);
-        void shareKeysWithTrustedUsersChanged(bool state);
-        void profileChanged(QString profile);
-        void userIdChanged(QString userId);
-        void accessTokenChanged(QString accessToken);
-        void deviceIdChanged(QString deviceId);
-        void homeserverChanged(QString homeserver);
-        void disableCertificateValidationChanged(bool disabled);
+    void groupViewStateChanged(bool state);
+    void roomSortingChanged(bool state);
+    void themeChanged(QString state);
+    void messageHoverHighlightChanged(bool state);
+    void enlargeEmojiOnlyMessagesChanged(bool state);
+    void trayChanged(bool state);
+    void startInTrayChanged(bool state);
+    void markdownChanged(bool state);
+    void animateImagesOnHoverChanged(bool state);
+    void typingNotificationsChanged(bool state);
+    void buttonInTimelineChanged(bool state);
+    void readReceiptsChanged(bool state);
+    void desktopNotificationsChanged(bool state);
+    void alertOnNotificationChanged(bool state);
+    void avatarCirclesChanged(bool state);
+    void decryptSidebarChanged(bool state);
+    void privacyScreenChanged(bool state);
+    void privacyScreenTimeoutChanged(int state);
+    void timelineMaxWidthChanged(int state);
+    void roomListWidthChanged(int state);
+    void communityListWidthChanged(int state);
+    void mobileModeChanged(bool mode);
+    void fontSizeChanged(double state);
+    void fontChanged(QString state);
+    void emojiFontChanged(QString state);
+    void presenceChanged(Presence state);
+    void ringtoneChanged(QString ringtone);
+    void microphoneChanged(QString microphone);
+    void cameraChanged(QString camera);
+    void cameraResolutionChanged(QString resolution);
+    void cameraFrameRateChanged(QString frameRate);
+    void screenShareFrameRateChanged(int frameRate);
+    void screenSharePiPChanged(bool state);
+    void screenShareRemoteVideoChanged(bool state);
+    void screenShareHideCursorChanged(bool state);
+    void useStunServerChanged(bool state);
+    void onlyShareKeysWithVerifiedUsersChanged(bool state);
+    void shareKeysWithTrustedUsersChanged(bool state);
+    void useOnlineKeyBackupChanged(bool state);
+    void profileChanged(QString profile);
+    void userIdChanged(QString userId);
+    void accessTokenChanged(QString accessToken);
+    void deviceIdChanged(QString deviceId);
+    void homeserverChanged(QString homeserver);
+    void disableCertificateValidationChanged(bool disabled);
+    void useIdenticonChanged(bool state);
 
 private:
-        // Default to system theme if QT_QPA_PLATFORMTHEME var is set.
-        QString defaultTheme_ =
-          QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty()
-            ? "light"
-            : "system";
-        QString theme_;
-        bool messageHoverHighlight_;
-        bool enlargeEmojiOnlyMessages_;
-        bool tray_;
-        bool startInTray_;
-        bool groupView_;
-        bool markdown_;
-        bool typingNotifications_;
-        bool sortByImportance_;
-        bool buttonsInTimeline_;
-        bool readReceipts_;
-        bool hasDesktopNotifications_;
-        bool hasAlertOnNotification_;
-        bool avatarCircles_;
-        bool decryptSidebar_;
-        bool privacyScreen_;
-        int privacyScreenTimeout_;
-        bool shareKeysWithTrustedUsers_;
-        bool onlyShareKeysWithVerifiedUsers_;
-        bool mobileMode_;
-        int timelineMaxWidth_;
-        int roomListWidth_;
-        int communityListWidth_;
-        double baseFontSize_;
-        QString font_;
-        QString emojiFont_;
-        Presence presence_;
-        QString ringtone_;
-        QString microphone_;
-        QString camera_;
-        QString cameraResolution_;
-        QString cameraFrameRate_;
-        int screenShareFrameRate_;
-        bool screenSharePiP_;
-        bool screenShareRemoteVideo_;
-        bool screenShareHideCursor_;
-        bool useStunServer_;
-        bool disableCertificateValidation_ = false;
-        QString profile_;
-        QString userId_;
-        QString accessToken_;
-        QString deviceId_;
-        QString homeserver_;
-        QStringList hiddenTags_;
-
-        QSettings settings;
-
-        static QSharedPointer<UserSettings> instance_;
+    // Default to system theme if QT_QPA_PLATFORMTHEME var is set.
+    QString defaultTheme_ =
+      QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty()
+        ? "light"
+        : "system";
+    QString theme_;
+    bool messageHoverHighlight_;
+    bool enlargeEmojiOnlyMessages_;
+    bool tray_;
+    bool startInTray_;
+    bool groupView_;
+    bool markdown_;
+    bool animateImagesOnHover_;
+    bool typingNotifications_;
+    bool sortByImportance_;
+    bool buttonsInTimeline_;
+    bool readReceipts_;
+    bool hasDesktopNotifications_;
+    bool hasAlertOnNotification_;
+    bool avatarCircles_;
+    bool decryptSidebar_;
+    bool privacyScreen_;
+    int privacyScreenTimeout_;
+    bool shareKeysWithTrustedUsers_;
+    bool onlyShareKeysWithVerifiedUsers_;
+    bool useOnlineKeyBackup_;
+    bool mobileMode_;
+    int timelineMaxWidth_;
+    int roomListWidth_;
+    int communityListWidth_;
+    double baseFontSize_;
+    QString font_;
+    QString emojiFont_;
+    Presence presence_;
+    QString ringtone_;
+    QString microphone_;
+    QString camera_;
+    QString cameraResolution_;
+    QString cameraFrameRate_;
+    int screenShareFrameRate_;
+    bool screenSharePiP_;
+    bool screenShareRemoteVideo_;
+    bool screenShareHideCursor_;
+    bool useStunServer_;
+    bool disableCertificateValidation_ = false;
+    QString profile_;
+    QString userId_;
+    QString accessToken_;
+    QString deviceId_;
+    QString homeserver_;
+    QStringList hiddenTags_;
+    bool useIdenticon_;
+
+    QSettings settings;
+
+    static QSharedPointer<UserSettings> instance_;
 };
 
 class HorizontalLine : public QFrame
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        HorizontalLine(QWidget *parent = nullptr);
+    HorizontalLine(QWidget *parent = nullptr);
 };
 
 class UserSettingsPage : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = nullptr);
+    UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = nullptr);
 
 protected:
-        void showEvent(QShowEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
+    void showEvent(QShowEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 signals:
-        void moveBack();
-        void trayOptionChanged(bool value);
-        void themeChanged();
-        void decryptSidebarChanged();
+    void moveBack();
+    void trayOptionChanged(bool value);
+    void themeChanged();
+    void decryptSidebarChanged();
 
 public slots:
-        void updateSecretStatus();
+    void updateSecretStatus();
 
 private slots:
-        void importSessionKeys();
-        void exportSessionKeys();
+    void importSessionKeys();
+    void exportSessionKeys();
 
 private:
-        // Layouts
-        QVBoxLayout *topLayout_;
-        QHBoxLayout *topBarLayout_;
-        QFormLayout *formLayout_;
-
-        // Shared settings object.
-        QSharedPointer<UserSettings> settings_;
-
-        Toggle *trayToggle_;
-        Toggle *startInTrayToggle_;
-        Toggle *groupViewToggle_;
-        Toggle *timelineButtonsToggle_;
-        Toggle *typingNotifications_;
-        Toggle *messageHoverHighlight_;
-        Toggle *enlargeEmojiOnlyMessages_;
-        Toggle *sortByImportance_;
-        Toggle *readReceipts_;
-        Toggle *markdown_;
-        Toggle *desktopNotifications_;
-        Toggle *alertOnNotification_;
-        Toggle *avatarCircles_;
-        Toggle *useStunServer_;
-        Toggle *decryptSidebar_;
-        Toggle *privacyScreen_;
-        QSpinBox *privacyScreenTimeout_;
-        Toggle *shareKeysWithTrustedUsers_;
-        Toggle *onlyShareKeysWithVerifiedUsers_;
-        Toggle *mobileMode_;
-        QLabel *deviceFingerprintValue_;
-        QLabel *deviceIdValue_;
-        QLabel *backupSecretCached;
-        QLabel *masterSecretCached;
-        QLabel *selfSigningSecretCached;
-        QLabel *userSigningSecretCached;
-
-        QComboBox *themeCombo_;
-        QComboBox *scaleFactorCombo_;
-        QComboBox *fontSizeCombo_;
-        QFontComboBox *fontSelectionCombo_;
-        QComboBox *emojiFontSelectionCombo_;
-        QComboBox *ringtoneCombo_;
-        QComboBox *microphoneCombo_;
-        QComboBox *cameraCombo_;
-        QComboBox *cameraResolutionCombo_;
-        QComboBox *cameraFrameRateCombo_;
-
-        QSpinBox *timelineMaxWidthSpin_;
-
-        int sideMargin_ = 0;
+    // Layouts
+    QVBoxLayout *topLayout_;
+    QHBoxLayout *topBarLayout_;
+    QFormLayout *formLayout_;
+
+    // Shared settings object.
+    QSharedPointer<UserSettings> settings_;
+
+    Toggle *trayToggle_;
+    Toggle *startInTrayToggle_;
+    Toggle *groupViewToggle_;
+    Toggle *timelineButtonsToggle_;
+    Toggle *typingNotifications_;
+    Toggle *messageHoverHighlight_;
+    Toggle *enlargeEmojiOnlyMessages_;
+    Toggle *sortByImportance_;
+    Toggle *readReceipts_;
+    Toggle *markdown_;
+    Toggle *animateImagesOnHover_;
+    Toggle *desktopNotifications_;
+    Toggle *alertOnNotification_;
+    Toggle *avatarCircles_;
+    Toggle *useIdenticon_;
+    Toggle *useStunServer_;
+    Toggle *decryptSidebar_;
+    Toggle *privacyScreen_;
+    QSpinBox *privacyScreenTimeout_;
+    Toggle *shareKeysWithTrustedUsers_;
+    Toggle *onlyShareKeysWithVerifiedUsers_;
+    Toggle *useOnlineKeyBackup_;
+    Toggle *mobileMode_;
+    QLabel *deviceFingerprintValue_;
+    QLabel *deviceIdValue_;
+    QLabel *backupSecretCached;
+    QLabel *masterSecretCached;
+    QLabel *selfSigningSecretCached;
+    QLabel *userSigningSecretCached;
+
+    QComboBox *themeCombo_;
+    QComboBox *scaleFactorCombo_;
+    QComboBox *fontSizeCombo_;
+    QFontComboBox *fontSelectionCombo_;
+    QComboBox *emojiFontSelectionCombo_;
+    QComboBox *ringtoneCombo_;
+    QComboBox *microphoneCombo_;
+    QComboBox *cameraCombo_;
+    QComboBox *cameraResolutionCombo_;
+    QComboBox *cameraFrameRateCombo_;
+
+    QSpinBox *timelineMaxWidthSpin_;
+
+    int sideMargin_ = 0;
 };
diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp
index c437966823ee5a201815be805a49cb6c6f49ae06..f82353cc376e622783221081bf8ae4e5b5f9997b 100644
--- a/src/UsersModel.cpp
+++ b/src/UsersModel.cpp
@@ -14,50 +14,51 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent)
   : QAbstractListModel(parent)
   , room_id(roomId)
 {
-        roomMembers_ = cache::roomMembers(roomId);
-        for (const auto &m : roomMembers_) {
-                displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m)));
-                userids.push_back(QString::fromStdString(m));
-        }
+    roomMembers_ = cache::roomMembers(roomId);
+    for (const auto &m : roomMembers_) {
+        displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m)));
+        userids.push_back(QString::fromStdString(m));
+    }
 }
 
 QHash<int, QByteArray>
 UsersModel::roleNames() const
 {
-        return {
-          {CompletionModel::CompletionRole, "completionRole"},
-          {CompletionModel::SearchRole, "searchRole"},
-          {CompletionModel::SearchRole2, "searchRole2"},
-          {Roles::DisplayName, "displayName"},
-          {Roles::AvatarUrl, "avatarUrl"},
-          {Roles::UserID, "userid"},
-        };
+    return {
+      {CompletionModel::CompletionRole, "completionRole"},
+      {CompletionModel::SearchRole, "searchRole"},
+      {CompletionModel::SearchRole2, "searchRole2"},
+      {Roles::DisplayName, "displayName"},
+      {Roles::AvatarUrl, "avatarUrl"},
+      {Roles::UserID, "userid"},
+    };
 }
 
 QVariant
 UsersModel::data(const QModelIndex &index, int role) const
 {
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                switch (role) {
-                case CompletionModel::CompletionRole:
-                        if (UserSettings::instance()->markdown())
-                                return QString("[%1](https://matrix.to/#/%2)")
-                                  .arg(displayNames[index.row()])
-                                  .arg(QString(QUrl::toPercentEncoding(userids[index.row()])));
-                        else
-                                return displayNames[index.row()];
-                case CompletionModel::SearchRole:
-                case Qt::DisplayRole:
-                case Roles::DisplayName:
-                        return displayNames[index.row()];
-                case CompletionModel::SearchRole2:
-                        return userids[index.row()];
-                case Roles::AvatarUrl:
-                        return cache::avatarUrl(QString::fromStdString(room_id),
-                                                QString::fromStdString(roomMembers_[index.row()]));
-                case Roles::UserID:
-                        return userids[index.row()];
-                }
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        switch (role) {
+        case CompletionModel::CompletionRole:
+            if (UserSettings::instance()->markdown())
+                return QString("[%1](https://matrix.to/#/%2)")
+                  .arg(displayNames[index.row()].toHtmlEscaped())
+                  .arg(QString(QUrl::toPercentEncoding(userids[index.row()])));
+            else
+                return displayNames[index.row()];
+        case CompletionModel::SearchRole:
+            return displayNames[index.row()];
+        case Qt::DisplayRole:
+        case Roles::DisplayName:
+            return displayNames[index.row()].toHtmlEscaped();
+        case CompletionModel::SearchRole2:
+            return userids[index.row()];
+        case Roles::AvatarUrl:
+            return cache::avatarUrl(QString::fromStdString(room_id),
+                                    QString::fromStdString(roomMembers_[index.row()]));
+        case Roles::UserID:
+            return userids[index.row()].toHtmlEscaped();
         }
-        return {};
+    }
+    return {};
 }
diff --git a/src/UsersModel.h b/src/UsersModel.h
index 5bc94b0fb665a608c4a66185f6a609c93fcd363c..e719a8bd794206cefea9b44531d941faf72d0be3 100644
--- a/src/UsersModel.h
+++ b/src/UsersModel.h
@@ -9,25 +9,25 @@
 class UsersModel : public QAbstractListModel
 {
 public:
-        enum Roles
-        {
-                AvatarUrl = Qt::UserRole,
-                DisplayName,
-                UserID,
-        };
+    enum Roles
+    {
+        AvatarUrl = Qt::UserRole,
+        DisplayName,
+        UserID,
+    };
 
-        UsersModel(const std::string &roomId, QObject *parent = nullptr);
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override
-        {
-                (void)parent;
-                return (int)roomMembers_.size();
-        }
-        QVariant data(const QModelIndex &index, int role) const override;
+    UsersModel(const std::string &roomId, QObject *parent = nullptr);
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        (void)parent;
+        return (int)roomMembers_.size();
+    }
+    QVariant data(const QModelIndex &index, int role) const override;
 
 private:
-        std::string room_id;
-        std::vector<std::string> roomMembers_;
-        std::vector<QString> displayNames;
-        std::vector<QString> userids;
+    std::string room_id;
+    std::vector<std::string> roomMembers_;
+    std::vector<QString> displayNames;
+    std::vector<QString> userids;
 };
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 41013e39ead5c38b38a6b3b5a8b207c01fd309a0..b0fb01b150dbf0f6a20ea32fb4a38b9e2fd8a5e6 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -38,154 +38,154 @@ template<class T, class Event>
 static DescInfo
 createDescriptionInfo(const Event &event, const QString &localUser, const QString &displayName)
 {
-        const auto msg    = std::get<T>(event);
-        const auto sender = QString::fromStdString(msg.sender);
+    const auto msg    = std::get<T>(event);
+    const auto sender = QString::fromStdString(msg.sender);
 
-        const auto username = displayName;
-        const auto ts       = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
-        auto body           = utils::event_body(event).trimmed();
-        if (mtx::accessors::relations(event).reply_to())
-                body = QString::fromStdString(utils::stripReplyFromBody(body.toStdString()));
+    const auto username = displayName;
+    const auto ts       = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
+    auto body           = utils::event_body(event).trimmed();
+    if (mtx::accessors::relations(event).reply_to())
+        body = QString::fromStdString(utils::stripReplyFromBody(body.toStdString()));
 
-        return DescInfo{QString::fromStdString(msg.event_id),
-                        sender,
-                        utils::messageDescription<T>(username, body, sender == localUser),
-                        utils::descriptiveTime(ts),
-                        msg.origin_server_ts,
-                        ts};
+    return DescInfo{QString::fromStdString(msg.event_id),
+                    sender,
+                    utils::messageDescription<T>(username, body, sender == localUser),
+                    utils::descriptiveTime(ts),
+                    msg.origin_server_ts,
+                    ts};
 }
 
 std::string
 utils::stripReplyFromBody(const std::string &bodyi)
 {
-        QString body = QString::fromStdString(bodyi);
-        QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
-        while (body.startsWith(">"))
-                body.remove(plainQuote);
-        if (body.startsWith("\n"))
-                body.remove(0, 1);
-        return body.toStdString();
+    QString body = QString::fromStdString(bodyi);
+    QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
+    while (body.startsWith(">"))
+        body.remove(plainQuote);
+    if (body.startsWith("\n"))
+        body.remove(0, 1);
+
+    body.replace("@room", QString::fromUtf8("@\u2060room"));
+    return body.toStdString();
 }
 
 std::string
 utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi)
 {
-        QString formatted_body = QString::fromStdString(formatted_bodyi);
-        formatted_body.remove(QRegularExpression("<mx-reply>.*</mx-reply>",
-                                                 QRegularExpression::DotMatchesEverythingOption));
-        formatted_body.replace("@room", "@\u2060room");
-        return formatted_body.toStdString();
+    QString formatted_body = QString::fromStdString(formatted_bodyi);
+    formatted_body.remove(QRegularExpression("<mx-reply>.*</mx-reply>",
+                                             QRegularExpression::DotMatchesEverythingOption));
+    formatted_body.replace("@room", QString::fromUtf8("@\u2060room"));
+    return formatted_body.toStdString();
 }
 
 RelatedInfo
 utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_)
 {
-        RelatedInfo related   = {};
-        related.quoted_user   = QString::fromStdString(mtx::accessors::sender(event));
-        related.related_event = std::move(id);
-        related.type          = mtx::accessors::msg_type(event);
+    RelatedInfo related   = {};
+    related.quoted_user   = QString::fromStdString(mtx::accessors::sender(event));
+    related.related_event = std::move(id);
+    related.type          = mtx::accessors::msg_type(event);
 
-        // get body, strip reply fallback, then transform the event to text, if it is a media event
-        // etc
-        related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
-        related.quoted_body =
-          QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString()));
-        related.quoted_body = utils::getQuoteBody(related);
-        related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room"));
+    // get body, strip reply fallback, then transform the event to text, if it is a media event
+    // etc
+    related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
+    related.quoted_body =
+      QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString()));
+    related.quoted_body = utils::getQuoteBody(related);
 
-        // get quoted body and strip reply fallback
-        related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
-        related.quoted_formatted_body = QString::fromStdString(
-          stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString()));
-        related.room = room_id_;
+    // get quoted body and strip reply fallback
+    related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
+    related.quoted_formatted_body = QString::fromStdString(
+      stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString()));
+    related.room = room_id_;
 
-        return related;
+    return related;
 }
 
 QString
 utils::localUser()
 {
-        return QString::fromStdString(http::client()->user_id().to_string());
+    return QString::fromStdString(http::client()->user_id().to_string());
 }
 
 bool
 utils::codepointIsEmoji(uint code)
 {
-        // TODO: Be more precise here.
-        return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) ||
-               (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f;
+    // TODO: Be more precise here.
+    return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) ||
+           (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f;
 }
 
 QString
 utils::replaceEmoji(const QString &body)
 {
-        QString fmtBody;
-        fmtBody.reserve(body.size());
-
-        QVector<uint> utf32_string = body.toUcs4();
-
-        bool insideFontBlock = false;
-        for (auto &code : utf32_string) {
-                if (utils::codepointIsEmoji(code)) {
-                        if (!insideFontBlock) {
-                                fmtBody += QStringLiteral("<font face=\"") %
-                                           UserSettings::instance()->emojiFont() %
-                                           QStringLiteral("\">");
-                                insideFontBlock = true;
-                        }
-
-                } else {
-                        if (insideFontBlock) {
-                                fmtBody += QStringLiteral("</font>");
-                                insideFontBlock = false;
-                        }
-                }
-                if (QChar::requiresSurrogates(code)) {
-                        QChar emoji[] = {static_cast<ushort>(QChar::highSurrogate(code)),
-                                         static_cast<ushort>(QChar::lowSurrogate(code))};
-                        fmtBody.append(emoji, 2);
-                } else {
-                        fmtBody.append(QChar(static_cast<ushort>(code)));
-                }
-        }
-        if (insideFontBlock) {
+    QString fmtBody;
+    fmtBody.reserve(body.size());
+
+    QVector<uint> utf32_string = body.toUcs4();
+
+    bool insideFontBlock = false;
+    for (auto &code : utf32_string) {
+        if (utils::codepointIsEmoji(code)) {
+            if (!insideFontBlock) {
+                fmtBody += QStringLiteral("<font face=\"") % UserSettings::instance()->emojiFont() %
+                           QStringLiteral("\">");
+                insideFontBlock = true;
+            }
+
+        } else {
+            if (insideFontBlock) {
                 fmtBody += QStringLiteral("</font>");
+                insideFontBlock = false;
+            }
+        }
+        if (QChar::requiresSurrogates(code)) {
+            QChar emoji[] = {static_cast<ushort>(QChar::highSurrogate(code)),
+                             static_cast<ushort>(QChar::lowSurrogate(code))};
+            fmtBody.append(emoji, 2);
+        } else {
+            fmtBody.append(QChar(static_cast<ushort>(code)));
         }
+    }
+    if (insideFontBlock) {
+        fmtBody += QStringLiteral("</font>");
+    }
 
-        return fmtBody;
+    return fmtBody;
 }
 
 void
 utils::setScaleFactor(float factor)
 {
-        if (factor < 1 || factor > 3)
-                return;
+    if (factor < 1 || factor > 3)
+        return;
 
-        QSettings settings;
-        settings.setValue("settings/scale_factor", factor);
+    QSettings settings;
+    settings.setValue("settings/scale_factor", factor);
 }
 
 float
 utils::scaleFactor()
 {
-        QSettings settings;
-        return settings.value("settings/scale_factor", -1).toFloat();
+    QSettings settings;
+    return settings.value("settings/scale_factor", -1).toFloat();
 }
 
 QString
 utils::descriptiveTime(const QDateTime &then)
 {
-        const auto now  = QDateTime::currentDateTime();
-        const auto days = then.daysTo(now);
+    const auto now  = QDateTime::currentDateTime();
+    const auto days = then.daysTo(now);
 
-        if (days == 0)
-                return QLocale::system().toString(then.time(), QLocale::ShortFormat);
-        else if (days < 2)
-                return QString(QCoreApplication::translate("descriptiveTime", "Yesterday"));
-        else if (days < 7)
-                return then.toString("dddd");
+    if (days == 0)
+        return QLocale::system().toString(then.time(), QLocale::ShortFormat);
+    else if (days < 2)
+        return QString(QCoreApplication::translate("descriptiveTime", "Yesterday"));
+    else if (days < 7)
+        return then.toString("dddd");
 
-        return QLocale::system().toString(then.date(), QLocale::ShortFormat);
+    return QLocale::system().toString(then.date(), QLocale::ShortFormat);
 }
 
 DescInfo
@@ -193,630 +193,622 @@ utils::getMessageDescription(const TimelineEvent &event,
                              const QString &localUser,
                              const QString &displayName)
 {
-        using Audio      = mtx::events::RoomEvent<mtx::events::msg::Audio>;
-        using Emote      = mtx::events::RoomEvent<mtx::events::msg::Emote>;
-        using File       = mtx::events::RoomEvent<mtx::events::msg::File>;
-        using Image      = mtx::events::RoomEvent<mtx::events::msg::Image>;
-        using Notice     = mtx::events::RoomEvent<mtx::events::msg::Notice>;
-        using Text       = mtx::events::RoomEvent<mtx::events::msg::Text>;
-        using Video      = mtx::events::RoomEvent<mtx::events::msg::Video>;
-        using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>;
-        using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>;
-        using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>;
-        using Encrypted  = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
-
-        if (std::holds_alternative<Audio>(event)) {
-                return createDescriptionInfo<Audio>(event, localUser, displayName);
-        } else if (std::holds_alternative<Emote>(event)) {
-                return createDescriptionInfo<Emote>(event, localUser, displayName);
-        } else if (std::holds_alternative<File>(event)) {
-                return createDescriptionInfo<File>(event, localUser, displayName);
-        } else if (std::holds_alternative<Image>(event)) {
-                return createDescriptionInfo<Image>(event, localUser, displayName);
-        } else if (std::holds_alternative<Notice>(event)) {
-                return createDescriptionInfo<Notice>(event, localUser, displayName);
-        } else if (std::holds_alternative<Text>(event)) {
-                return createDescriptionInfo<Text>(event, localUser, displayName);
-        } else if (std::holds_alternative<Video>(event)) {
-                return createDescriptionInfo<Video>(event, localUser, displayName);
-        } else if (std::holds_alternative<CallInvite>(event)) {
-                return createDescriptionInfo<CallInvite>(event, localUser, displayName);
-        } else if (std::holds_alternative<CallAnswer>(event)) {
-                return createDescriptionInfo<CallAnswer>(event, localUser, displayName);
-        } else if (std::holds_alternative<CallHangUp>(event)) {
-                return createDescriptionInfo<CallHangUp>(event, localUser, displayName);
-        } else if (std::holds_alternative<mtx::events::Sticker>(event)) {
-                return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName);
-        } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
-                const auto sender = QString::fromStdString(msg->sender);
-
-                const auto username = displayName;
-                const auto ts       = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts);
-
-                DescInfo info;
-                info.userid = sender;
-                info.body   = QString(" %1").arg(
-                  messageDescription<Encrypted>(username, "", sender == localUser));
-                info.timestamp       = msg->origin_server_ts;
-                info.descriptiveTime = utils::descriptiveTime(ts);
-                info.event_id        = QString::fromStdString(msg->event_id);
-                info.datetime        = ts;
-
-                return info;
-        }
+    using Audio      = mtx::events::RoomEvent<mtx::events::msg::Audio>;
+    using Emote      = mtx::events::RoomEvent<mtx::events::msg::Emote>;
+    using File       = mtx::events::RoomEvent<mtx::events::msg::File>;
+    using Image      = mtx::events::RoomEvent<mtx::events::msg::Image>;
+    using Notice     = mtx::events::RoomEvent<mtx::events::msg::Notice>;
+    using Text       = mtx::events::RoomEvent<mtx::events::msg::Text>;
+    using Video      = mtx::events::RoomEvent<mtx::events::msg::Video>;
+    using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>;
+    using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>;
+    using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>;
+    using Encrypted  = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
+
+    if (std::holds_alternative<Audio>(event)) {
+        return createDescriptionInfo<Audio>(event, localUser, displayName);
+    } else if (std::holds_alternative<Emote>(event)) {
+        return createDescriptionInfo<Emote>(event, localUser, displayName);
+    } else if (std::holds_alternative<File>(event)) {
+        return createDescriptionInfo<File>(event, localUser, displayName);
+    } else if (std::holds_alternative<Image>(event)) {
+        return createDescriptionInfo<Image>(event, localUser, displayName);
+    } else if (std::holds_alternative<Notice>(event)) {
+        return createDescriptionInfo<Notice>(event, localUser, displayName);
+    } else if (std::holds_alternative<Text>(event)) {
+        return createDescriptionInfo<Text>(event, localUser, displayName);
+    } else if (std::holds_alternative<Video>(event)) {
+        return createDescriptionInfo<Video>(event, localUser, displayName);
+    } else if (std::holds_alternative<CallInvite>(event)) {
+        return createDescriptionInfo<CallInvite>(event, localUser, displayName);
+    } else if (std::holds_alternative<CallAnswer>(event)) {
+        return createDescriptionInfo<CallAnswer>(event, localUser, displayName);
+    } else if (std::holds_alternative<CallHangUp>(event)) {
+        return createDescriptionInfo<CallHangUp>(event, localUser, displayName);
+    } else if (std::holds_alternative<mtx::events::Sticker>(event)) {
+        return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName);
+    } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
+        const auto sender = QString::fromStdString(msg->sender);
+
+        const auto username = displayName;
+        const auto ts       = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts);
+
+        DescInfo info;
+        info.userid = sender;
+        info.body =
+          QString(" %1").arg(messageDescription<Encrypted>(username, "", sender == localUser));
+        info.timestamp       = msg->origin_server_ts;
+        info.descriptiveTime = utils::descriptiveTime(ts);
+        info.event_id        = QString::fromStdString(msg->event_id);
+        info.datetime        = ts;
 
-        return DescInfo{};
+        return info;
+    }
+
+    return DescInfo{};
 }
 
 QString
 utils::firstChar(const QString &input)
 {
-        if (input.isEmpty())
-                return input;
+    if (input.isEmpty())
+        return input;
 
-        for (auto const &c : input.toUcs4()) {
-                if (QString::fromUcs4(&c, 1) != QString("#"))
-                        return QString::fromUcs4(&c, 1).toUpper();
-        }
+    for (auto const &c : input.toUcs4()) {
+        if (QString::fromUcs4(&c, 1) != QString("#"))
+            return QString::fromUcs4(&c, 1).toUpper();
+    }
 
-        return QString::fromUcs4(&input.toUcs4().at(0), 1).toUpper();
+    return QString::fromUcs4(&input.toUcs4().at(0), 1).toUpper();
 }
 
 QString
 utils::humanReadableFileSize(uint64_t bytes)
 {
-        constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
-        constexpr static const int length    = sizeof(units) / sizeof(units[0]);
+    constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
+    constexpr static const int length    = sizeof(units) / sizeof(units[0]);
 
-        int u       = 0;
-        double size = static_cast<double>(bytes);
-        while (size >= 1024.0 && u < length) {
-                ++u;
-                size /= 1024.0;
-        }
+    int u       = 0;
+    double size = static_cast<double>(bytes);
+    while (size >= 1024.0 && u < length) {
+        ++u;
+        size /= 1024.0;
+    }
 
-        return QString::number(size, 'g', 4) + ' ' + units[u];
+    return QString::number(size, 'g', 4) + ' ' + units[u];
 }
 
 int
 utils::levenshtein_distance(const std::string &s1, const std::string &s2)
 {
-        const auto nlen = s1.size();
-        const auto hlen = s2.size();
+    const auto nlen = s1.size();
+    const auto hlen = s2.size();
 
-        if (hlen == 0)
-                return -1;
-        if (nlen == 1)
-                return (int)s2.find(s1);
+    if (hlen == 0)
+        return -1;
+    if (nlen == 1)
+        return (int)s2.find(s1);
 
-        std::vector<int> row1(hlen + 1, 0);
+    std::vector<int> row1(hlen + 1, 0);
 
-        for (size_t i = 0; i < nlen; ++i) {
-                std::vector<int> row2(1, (int)i + 1);
+    for (size_t i = 0; i < nlen; ++i) {
+        std::vector<int> row2(1, (int)i + 1);
 
-                for (size_t j = 0; j < hlen; ++j) {
-                        const int cost = s1[i] != s2[j];
-                        row2.push_back(
-                          std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost)));
-                }
-
-                row1.swap(row2);
+        for (size_t j = 0; j < hlen; ++j) {
+            const int cost = s1[i] != s2[j];
+            row2.push_back(std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost)));
         }
 
-        return *std::min_element(row1.begin(), row1.end());
+        row1.swap(row2);
+    }
+
+    return *std::min_element(row1.begin(), row1.end());
 }
 
 QString
 utils::event_body(const mtx::events::collections::TimelineEvents &e)
 {
-        using namespace mtx::events;
-        if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr)
-                return QString::fromStdString(ev->content.body);
-        if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr)
-                return QString::fromStdString(ev->content.body);
-        if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr)
-                return QString::fromStdString(ev->content.body);
-        if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr)
-                return QString::fromStdString(ev->content.body);
-        if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr)
-                return QString::fromStdString(ev->content.body);
-        if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr)
-                return QString::fromStdString(ev->content.body);
-        if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr)
-                return QString::fromStdString(ev->content.body);
-
-        return "";
+    using namespace mtx::events;
+    if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr)
+        return QString::fromStdString(ev->content.body);
+    if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr)
+        return QString::fromStdString(ev->content.body);
+    if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr)
+        return QString::fromStdString(ev->content.body);
+    if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr)
+        return QString::fromStdString(ev->content.body);
+    if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr)
+        return QString::fromStdString(ev->content.body);
+    if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr)
+        return QString::fromStdString(ev->content.body);
+    if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr)
+        return QString::fromStdString(ev->content.body);
+
+    return "";
 }
 
 QPixmap
 utils::scaleImageToPixmap(const QImage &img, int size)
 {
-        if (img.isNull())
-                return QPixmap();
+    if (img.isNull())
+        return QPixmap();
 
-        // Deprecated in 5.13: const double sz =
-        //  std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size);
-        const double sz =
-          std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size);
-        return QPixmap::fromImage(
-          img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
+    // Deprecated in 5.13: const double sz =
+    //  std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size);
+    const double sz =
+      std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size);
+    return QPixmap::fromImage(img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
 }
 
 QPixmap
 utils::scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source)
 {
-        if (source.isNull())
-                return QPixmap();
+    if (source.isNull())
+        return QPixmap();
 
-        const double widthRatio     = (double)maxWidth / (double)source.width();
-        const double heightRatio    = (double)maxHeight / (double)source.height();
-        const double minAspectRatio = std::min(widthRatio, heightRatio);
+    const double widthRatio     = (double)maxWidth / (double)source.width();
+    const double heightRatio    = (double)maxHeight / (double)source.height();
+    const double minAspectRatio = std::min(widthRatio, heightRatio);
 
-        // Size of the output image.
-        int w, h = 0;
+    // Size of the output image.
+    int w, h = 0;
 
-        if (minAspectRatio > 1) {
-                w = source.width();
-                h = source.height();
-        } else {
-                w = source.width() * minAspectRatio;
-                h = source.height() * minAspectRatio;
-        }
+    if (minAspectRatio > 1) {
+        w = source.width();
+        h = source.height();
+    } else {
+        w = source.width() * minAspectRatio;
+        h = source.height() * minAspectRatio;
+    }
 
-        return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+    return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
 }
 
 QString
 utils::mxcToHttp(const QUrl &url, const QString &server, int port)
 {
-        auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString());
+    auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString());
 
-        return QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
-          .arg(server)
-          .arg(port)
-          .arg(QString::fromStdString(mxcParts.server))
-          .arg(QString::fromStdString(mxcParts.media_id));
+    return QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
+      .arg(server)
+      .arg(port)
+      .arg(QString::fromStdString(mxcParts.server))
+      .arg(QString::fromStdString(mxcParts.media_id));
 }
 
 QString
 utils::humanReadableFingerprint(const std::string &ed25519)
 {
-        return humanReadableFingerprint(QString::fromStdString(ed25519));
+    return humanReadableFingerprint(QString::fromStdString(ed25519));
 }
 QString
 utils::humanReadableFingerprint(const QString &ed25519)
 {
-        QString fingerprint;
-        for (int i = 0; i < ed25519.length(); i = i + 4) {
-                fingerprint.append(ed25519.midRef(i, 4));
-                if (i > 0 && i % 16 == 12)
-                        fingerprint.append('\n');
-                else if (i < ed25519.length())
-                        fingerprint.append(' ');
-        }
-        return fingerprint;
+    QString fingerprint;
+    for (int i = 0; i < ed25519.length(); i = i + 4) {
+        fingerprint.append(ed25519.midRef(i, 4));
+        if (i > 0 && i % 16 == 12)
+            fingerprint.append('\n');
+        else if (i < ed25519.length())
+            fingerprint.append(' ');
+    }
+    return fingerprint;
 }
 
 QString
 utils::linkifyMessage(const QString &body)
 {
-        // Convert to valid XML.
-        auto doc = body;
-        doc.replace(conf::strings::url_regex, conf::strings::url_html);
-        doc.replace(QRegularExpression("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b"),
-                    conf::strings::url_html);
+    // Convert to valid XML.
+    auto doc = body;
+    doc.replace(conf::strings::url_regex, conf::strings::url_html);
+    doc.replace(QRegularExpression("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b"),
+                conf::strings::url_html);
 
-        return doc;
+    return doc;
 }
 
 QString
 utils::escapeBlacklistedHtml(const QString &rawStr)
 {
-        static const std::array allowedTags = {
-          "font",       "/font",       "del",    "/del",    "h1",    "/h1",    "h2",     "/h2",
-          "h3",         "/h3",         "h4",     "/h4",     "h5",    "/h5",    "h6",     "/h6",
-          "blockquote", "/blockquote", "p",      "/p",      "a",     "/a",     "ul",     "/ul",
-          "ol",         "/ol",         "sup",    "/sup",    "sub",   "/sub",   "li",     "/li",
-          "b",          "/b",          "i",      "/i",      "u",     "/u",     "strong", "/strong",
-          "em",         "/em",         "strike", "/strike", "code",  "/code",  "hr",     "/hr",
-          "br",         "br/",         "div",    "/div",    "table", "/table", "thead",  "/thead",
-          "tbody",      "/tbody",      "tr",     "/tr",     "th",    "/th",    "td",     "/td",
-          "caption",    "/caption",    "pre",    "/pre",    "span",  "/span",  "img",    "/img"};
-        QByteArray data = rawStr.toUtf8();
-        QByteArray buffer;
-        const int length = data.size();
-        buffer.reserve(length);
-        bool escapingTag = false;
-        for (int pos = 0; pos != length; ++pos) {
-                switch (data.at(pos)) {
-                case '<': {
-                        bool oneTagMatched = false;
-                        const int endPos =
-                          static_cast<int>(std::min(static_cast<size_t>(data.indexOf('>', pos)),
-                                                    static_cast<size_t>(data.indexOf(' ', pos))));
-
-                        auto mid = data.mid(pos + 1, endPos - pos - 1);
-                        for (const auto &tag : allowedTags) {
-                                // TODO: Check src and href attribute
-                                if (mid.toLower() == tag) {
-                                        oneTagMatched = true;
-                                }
-                        }
-                        if (oneTagMatched)
-                                buffer.append('<');
-                        else {
-                                escapingTag = true;
-                                buffer.append("&lt;");
-                        }
-                        break;
-                }
-                case '>':
-                        if (escapingTag) {
-                                buffer.append("&gt;");
-                                escapingTag = false;
-                        } else
-                                buffer.append('>');
-                        break;
-                default:
-                        buffer.append(data.at(pos));
-                        break;
+    static const std::array allowedTags = {
+      "font",       "/font",       "del",    "/del",    "h1",    "/h1",    "h2",     "/h2",
+      "h3",         "/h3",         "h4",     "/h4",     "h5",    "/h5",    "h6",     "/h6",
+      "blockquote", "/blockquote", "p",      "/p",      "a",     "/a",     "ul",     "/ul",
+      "ol",         "/ol",         "sup",    "/sup",    "sub",   "/sub",   "li",     "/li",
+      "b",          "/b",          "i",      "/i",      "u",     "/u",     "strong", "/strong",
+      "em",         "/em",         "strike", "/strike", "code",  "/code",  "hr",     "/hr",
+      "br",         "br/",         "div",    "/div",    "table", "/table", "thead",  "/thead",
+      "tbody",      "/tbody",      "tr",     "/tr",     "th",    "/th",    "td",     "/td",
+      "caption",    "/caption",    "pre",    "/pre",    "span",  "/span",  "img",    "/img"};
+    QByteArray data = rawStr.toUtf8();
+    QByteArray buffer;
+    const int length = data.size();
+    buffer.reserve(length);
+    bool escapingTag = false;
+    for (int pos = 0; pos != length; ++pos) {
+        switch (data.at(pos)) {
+        case '<': {
+            bool oneTagMatched = false;
+            const int endPos =
+              static_cast<int>(std::min(static_cast<size_t>(data.indexOf('>', pos)),
+                                        static_cast<size_t>(data.indexOf(' ', pos))));
+
+            auto mid = data.mid(pos + 1, endPos - pos - 1);
+            for (const auto &tag : allowedTags) {
+                // TODO: Check src and href attribute
+                if (mid.toLower() == tag) {
+                    oneTagMatched = true;
                 }
+            }
+            if (oneTagMatched)
+                buffer.append('<');
+            else {
+                escapingTag = true;
+                buffer.append("&lt;");
+            }
+            break;
+        }
+        case '>':
+            if (escapingTag) {
+                buffer.append("&gt;");
+                escapingTag = false;
+            } else
+                buffer.append('>');
+            break;
+        default:
+            buffer.append(data.at(pos));
+            break;
         }
-        return QString::fromUtf8(buffer);
+    }
+    return QString::fromUtf8(buffer);
 }
 
 QString
 utils::markdownToHtml(const QString &text, bool rainbowify)
 {
-        const auto str = text.toUtf8();
-        cmark_node *const node =
-          cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE);
-
-        if (rainbowify) {
-                // create iterator over node
-                cmark_iter *iter = cmark_iter_new(node);
-
-                cmark_event_type ev_type;
-
-                // First loop to get total text length
-                int textLen = 0;
-                while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
-                        cmark_node *cur = cmark_iter_get_node(iter);
-                        // only text nodes (no code or semilar)
-                        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
-                                continue;
-                        // count up by length of current node's text
-                        QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme,
-                                                QString(cmark_node_get_literal(cur)));
-                        while (tbf.toNextBoundary() != -1)
-                                textLen++;
-                }
+    const auto str         = text.toUtf8();
+    cmark_node *const node = cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE);
+
+    if (rainbowify) {
+        // create iterator over node
+        cmark_iter *iter = cmark_iter_new(node);
+
+        cmark_event_type ev_type;
+
+        // First loop to get total text length
+        int textLen = 0;
+        while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
+            cmark_node *cur = cmark_iter_get_node(iter);
+            // only text nodes (no code or semilar)
+            if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
+                continue;
+            // count up by length of current node's text
+            QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme,
+                                    QString(cmark_node_get_literal(cur)));
+            while (tbf.toNextBoundary() != -1)
+                textLen++;
+        }
 
-                // create new iter to start over
-                cmark_iter_free(iter);
-                iter = cmark_iter_new(node);
-
-                // Second loop to rainbowify
-                int charIdx = 0;
-                while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
-                        cmark_node *cur = cmark_iter_get_node(iter);
-                        // only text nodes (no code or semilar)
-                        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
-                                continue;
-
-                        // get text in current node
-                        QString nodeText(cmark_node_get_literal(cur));
-                        // create buffer to append rainbow text to
-                        QString buf;
-                        int boundaryStart = 0;
-                        int boundaryEnd   = 0;
-                        // use QTextBoundaryFinder to iterate ofer graphemes
-                        QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme,
-                                                nodeText);
-                        while ((boundaryEnd = tbf.toNextBoundary()) != -1) {
-                                charIdx++;
-                                // Split text to get current char
-                                auto curChar =
-                                  nodeText.midRef(boundaryStart, boundaryEnd - boundaryStart);
-                                boundaryStart = boundaryEnd;
-                                // Don't rainbowify whitespaces
-                                if (curChar.trimmed().isEmpty() ||
-                                    codepointIsEmoji(curChar.toUcs4().first())) {
-                                        buf.append(curChar);
-                                        continue;
-                                }
-
-                                // get correct color for char index
-                                // Use colors as described here:
-                                // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
-                                auto color =
-                                  QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
-                                // format color for HTML
-                                auto colorString = color.name(QColor::NameFormat::HexRgb);
-                                // create HTML element for current char
-                                auto curCharColored = QString("<font color=\"%0\">%1</font>")
-                                                        .arg(colorString)
-                                                        .arg(curChar);
-                                // append colored HTML element to buffer
-                                buf.append(curCharColored);
-                        }
-
-                        // create HTML_INLINE node to prevent HTML from being escaped
-                        auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE);
-                        // set content of HTML node to buffer contents
-                        cmark_node_set_literal(htmlNode, buf.toUtf8().data());
-                        // replace current node with HTML node
-                        cmark_node_replace(cur, htmlNode);
-                        // free memory of old node
-                        cmark_node_free(cur);
+        // create new iter to start over
+        cmark_iter_free(iter);
+        iter = cmark_iter_new(node);
+
+        // Second loop to rainbowify
+        int charIdx = 0;
+        while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
+            cmark_node *cur = cmark_iter_get_node(iter);
+            // only text nodes (no code or semilar)
+            if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
+                continue;
+
+            // get text in current node
+            QString nodeText(cmark_node_get_literal(cur));
+            // create buffer to append rainbow text to
+            QString buf;
+            int boundaryStart = 0;
+            int boundaryEnd   = 0;
+            // use QTextBoundaryFinder to iterate ofer graphemes
+            QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, nodeText);
+            while ((boundaryEnd = tbf.toNextBoundary()) != -1) {
+                charIdx++;
+                // Split text to get current char
+                auto curChar  = nodeText.midRef(boundaryStart, boundaryEnd - boundaryStart);
+                boundaryStart = boundaryEnd;
+                // Don't rainbowify whitespaces
+                if (curChar.trimmed().isEmpty() || codepointIsEmoji(curChar.toUcs4().first())) {
+                    buf.append(curChar);
+                    continue;
                 }
 
-                cmark_iter_free(iter);
+                // get correct color for char index
+                // Use colors as described here:
+                // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
+                auto color = QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
+                // format color for HTML
+                auto colorString = color.name(QColor::NameFormat::HexRgb);
+                // create HTML element for current char
+                auto curCharColored =
+                  QString("<font color=\"%0\">%1</font>").arg(colorString).arg(curChar);
+                // append colored HTML element to buffer
+                buf.append(curCharColored);
+            }
+
+            // create HTML_INLINE node to prevent HTML from being escaped
+            auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE);
+            // set content of HTML node to buffer contents
+            cmark_node_set_literal(htmlNode, buf.toUtf8().data());
+            // replace current node with HTML node
+            cmark_node_replace(cur, htmlNode);
+            // free memory of old node
+            cmark_node_free(cur);
         }
 
-        const char *tmp_buf = cmark_render_html(node, CMARK_OPT_UNSAFE);
-        // Copy the null terminated output buffer.
-        std::string html(tmp_buf);
-
-        // The buffer is no longer needed.
-        free((char *)tmp_buf);
+        cmark_iter_free(iter);
+    }
 
-        auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed();
+    const char *tmp_buf = cmark_render_html(node, CMARK_OPT_UNSAFE);
+    // Copy the null terminated output buffer.
+    std::string html(tmp_buf);
 
-        if (result.count("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</p>")) {
-                result = result.mid(3, result.size() - 3 - 4);
-        }
+    // The buffer is no longer needed.
+    free((char *)tmp_buf);
 
-        return result;
-}
+    auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed();
 
-QString
-utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
-{
-        auto getFormattedBody = [related]() -> QString {
-                using MsgType = mtx::events::MessageType;
+    if (result.count("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</p>")) {
+        result = result.mid(3, result.size() - 3 - 4);
+    }
 
-                switch (related.type) {
-                case MsgType::File: {
-                        return "sent a file.";
-                }
-                case MsgType::Image: {
-                        return "sent an image.";
-                }
-                case MsgType::Audio: {
-                        return "sent an audio file.";
-                }
-                case MsgType::Video: {
-                        return "sent a video";
-                }
-                default: {
-                        return related.quoted_formatted_body;
-                }
-                }
-        };
-        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"
-                       "/>%5</blockquote></mx-reply>")
-                 .arg(related.room,
-                      QString::fromStdString(related.related_event),
-                      related.quoted_user,
-                      related.quoted_user,
-                      getFormattedBody()) +
-               html;
+    return result;
 }
 
 QString
-utils::getQuoteBody(const RelatedInfo &related)
+utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
 {
+    auto getFormattedBody = [related]() -> QString {
         using MsgType = mtx::events::MessageType;
 
         switch (related.type) {
         case MsgType::File: {
-                return "sent a file.";
+            return "sent a file.";
         }
         case MsgType::Image: {
-                return "sent an image.";
+            return "sent an image.";
         }
         case MsgType::Audio: {
-                return "sent an audio file.";
+            return "sent an audio file.";
         }
         case MsgType::Video: {
-                return "sent a video";
+            return "sent a video";
         }
         default: {
-                return related.quoted_body;
+            return related.quoted_formatted_body;
         }
         }
+    };
+    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"
+                   "/>%5</blockquote></mx-reply>")
+             .arg(related.room,
+                  QString::fromStdString(related.related_event),
+                  related.quoted_user,
+                  related.quoted_user,
+                  getFormattedBody()) +
+           html;
+}
+
+QString
+utils::getQuoteBody(const RelatedInfo &related)
+{
+    using MsgType = mtx::events::MessageType;
+
+    switch (related.type) {
+    case MsgType::File: {
+        return "sent a file.";
+    }
+    case MsgType::Image: {
+        return "sent an image.";
+    }
+    case MsgType::Audio: {
+        return "sent an audio file.";
+    }
+    case MsgType::Video: {
+        return "sent a video";
+    }
+    default: {
+        return related.quoted_body;
+    }
+    }
 }
 
 QString
 utils::linkColor()
 {
-        const auto theme = UserSettings::instance()->theme();
+    const auto theme = UserSettings::instance()->theme();
 
-        if (theme == "light") {
-                return "#0077b5";
-        } else if (theme == "dark") {
-                return "#38A3D8";
-        } else {
-                return QPalette().color(QPalette::Link).name();
-        }
+    if (theme == "light") {
+        return "#0077b5";
+    } else if (theme == "dark") {
+        return "#38A3D8";
+    } else {
+        return QPalette().color(QPalette::Link).name();
+    }
 }
 
 uint32_t
 utils::hashQString(const QString &input)
 {
-        uint32_t hash = 0;
+    uint32_t hash = 0;
 
-        for (int i = 0; i < input.length(); i++) {
-                hash = input.at(i).digitValue() + ((hash << 5) - hash);
-        }
+    for (int i = 0; i < input.length(); i++) {
+        hash = input.at(i).digitValue() + ((hash << 5) - hash);
+    }
 
-        return hash;
+    return hash;
 }
 
 QColor
 utils::generateContrastingHexColor(const QString &input, const QColor &backgroundCol)
 {
-        const qreal backgroundLum = luminance(backgroundCol);
-
-        // Create a color for the input
-        auto hash = hashQString(input);
-        // create a hue value based on the hash of the input.
-        auto userHue = static_cast<int>(hash % 360);
-        // start with moderate saturation and lightness values.
-        auto sat       = 220;
-        auto lightness = 125;
-
-        // converting to a QColor makes the luminance calc easier.
-        QColor inputColor = QColor::fromHsl(userHue, sat, lightness);
-
-        // calculate the initial luminance and contrast of the
-        // generated color.  It's possible that no additional
-        // work will be necessary.
-        auto lum      = luminance(inputColor);
-        auto contrast = computeContrast(lum, backgroundLum);
-
-        // If the contrast doesn't meet our criteria,
-        // try again and again until they do by modifying first
-        // the lightness and then the saturation of the color.
-        int iterationCount = 9;
-        while (contrast < 5) {
-                // if our lightness is at it's bounds, try changing
-                // saturation instead.
-                if (lightness == 242 || lightness == 13) {
-                        qreal newSat = qBound(26.0, sat * 1.25, 242.0);
-
-                        inputColor.setHsl(userHue, qFloor(newSat), lightness);
-                        auto tmpLum         = luminance(inputColor);
-                        auto higherContrast = computeContrast(tmpLum, backgroundLum);
-                        if (higherContrast > contrast) {
-                                contrast = higherContrast;
-                                sat      = newSat;
-                        } else {
-                                newSat = qBound(26.0, sat / 1.25, 242.0);
-                                inputColor.setHsl(userHue, qFloor(newSat), lightness);
-                                tmpLum             = luminance(inputColor);
-                                auto lowerContrast = computeContrast(tmpLum, backgroundLum);
-                                if (lowerContrast > contrast) {
-                                        contrast = lowerContrast;
-                                        sat      = newSat;
-                                }
-                        }
-                } else {
-                        qreal newLightness = qBound(13.0, lightness * 1.25, 242.0);
-
-                        inputColor.setHsl(userHue, sat, qFloor(newLightness));
-
-                        auto tmpLum         = luminance(inputColor);
-                        auto higherContrast = computeContrast(tmpLum, backgroundLum);
-
-                        // Check to make sure we have actually improved contrast
-                        if (higherContrast > contrast) {
-                                contrast  = higherContrast;
-                                lightness = newLightness;
-                                // otherwise, try going the other way instead.
-                        } else {
-                                newLightness = qBound(13.0, lightness / 1.25, 242.0);
-                                inputColor.setHsl(userHue, sat, qFloor(newLightness));
-                                tmpLum             = luminance(inputColor);
-                                auto lowerContrast = computeContrast(tmpLum, backgroundLum);
-                                if (lowerContrast > contrast) {
-                                        contrast  = lowerContrast;
-                                        lightness = newLightness;
-                                }
-                        }
+    const qreal backgroundLum = luminance(backgroundCol);
+
+    // Create a color for the input
+    auto hash = hashQString(input);
+    // create a hue value based on the hash of the input.
+    auto userHue = static_cast<int>(hash % 360);
+    // start with moderate saturation and lightness values.
+    auto sat       = 220;
+    auto lightness = 125;
+
+    // converting to a QColor makes the luminance calc easier.
+    QColor inputColor = QColor::fromHsl(userHue, sat, lightness);
+
+    // calculate the initial luminance and contrast of the
+    // generated color.  It's possible that no additional
+    // work will be necessary.
+    auto lum      = luminance(inputColor);
+    auto contrast = computeContrast(lum, backgroundLum);
+
+    // If the contrast doesn't meet our criteria,
+    // try again and again until they do by modifying first
+    // the lightness and then the saturation of the color.
+    int iterationCount = 9;
+    while (contrast < 5) {
+        // if our lightness is at it's bounds, try changing
+        // saturation instead.
+        if (lightness == 242 || lightness == 13) {
+            qreal newSat = qBound(26.0, sat * 1.25, 242.0);
+
+            inputColor.setHsl(userHue, qFloor(newSat), lightness);
+            auto tmpLum         = luminance(inputColor);
+            auto higherContrast = computeContrast(tmpLum, backgroundLum);
+            if (higherContrast > contrast) {
+                contrast = higherContrast;
+                sat      = newSat;
+            } else {
+                newSat = qBound(26.0, sat / 1.25, 242.0);
+                inputColor.setHsl(userHue, qFloor(newSat), lightness);
+                tmpLum             = luminance(inputColor);
+                auto lowerContrast = computeContrast(tmpLum, backgroundLum);
+                if (lowerContrast > contrast) {
+                    contrast = lowerContrast;
+                    sat      = newSat;
                 }
-
-                // don't loop forever, just give up at some point!
-                // Someone smart may find a better solution
-                if (--iterationCount < 0)
-                        break;
+            }
+        } else {
+            qreal newLightness = qBound(13.0, lightness * 1.25, 242.0);
+
+            inputColor.setHsl(userHue, sat, qFloor(newLightness));
+
+            auto tmpLum         = luminance(inputColor);
+            auto higherContrast = computeContrast(tmpLum, backgroundLum);
+
+            // Check to make sure we have actually improved contrast
+            if (higherContrast > contrast) {
+                contrast  = higherContrast;
+                lightness = newLightness;
+                // otherwise, try going the other way instead.
+            } else {
+                newLightness = qBound(13.0, lightness / 1.25, 242.0);
+                inputColor.setHsl(userHue, sat, qFloor(newLightness));
+                tmpLum             = luminance(inputColor);
+                auto lowerContrast = computeContrast(tmpLum, backgroundLum);
+                if (lowerContrast > contrast) {
+                    contrast  = lowerContrast;
+                    lightness = newLightness;
+                }
+            }
         }
 
-        // get the hex value of the generated color.
-        auto colorHex = inputColor.name();
+        // don't loop forever, just give up at some point!
+        // Someone smart may find a better solution
+        if (--iterationCount < 0)
+            break;
+    }
 
-        return colorHex;
+    // get the hex value of the generated color.
+    auto colorHex = inputColor.name();
+
+    return colorHex;
 }
 
 qreal
 utils::computeContrast(const qreal &one, const qreal &two)
 {
-        auto ratio = (one + 0.05) / (two + 0.05);
+    auto ratio = (one + 0.05) / (two + 0.05);
 
-        if (two > one) {
-                ratio = 1 / ratio;
-        }
+    if (two > one) {
+        ratio = 1 / ratio;
+    }
 
-        return ratio;
+    return ratio;
 }
 
 qreal
 utils::luminance(const QColor &col)
 {
-        int colRgb[3] = {col.red(), col.green(), col.blue()};
-        qreal lumRgb[3];
+    int colRgb[3] = {col.red(), col.green(), col.blue()};
+    qreal lumRgb[3];
 
-        for (int i = 0; i < 3; i++) {
-                qreal v   = colRgb[i] / 255.0;
-                lumRgb[i] = v <= 0.03928 ? v / 12.92 : qPow((v + 0.055) / 1.055, 2.4);
-        }
+    for (int i = 0; i < 3; i++) {
+        qreal v   = colRgb[i] / 255.0;
+        lumRgb[i] = v <= 0.03928 ? v / 12.92 : qPow((v + 0.055) / 1.055, 2.4);
+    }
 
-        auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
+    auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
 
-        return lum;
+    return lum;
 }
 
 void
 utils::centerWidget(QWidget *widget, QWidget *parent)
 {
-        auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
-                return QPoint(hostRect.center().x() - (childRect.width() * 0.5),
-                              hostRect.center().y() - (childRect.height() * 0.5));
-        };
+    auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
+        return QPoint(hostRect.center().x() - (childRect.width() * 0.5),
+                      hostRect.center().y() - (childRect.height() * 0.5));
+    };
 
-        if (parent) {
-                widget->move(parent->window()->frameGeometry().topLeft() +
-                             parent->window()->rect().center() - widget->rect().center());
-                return;
-        }
+    if (parent) {
+        widget->move(parent->window()->frameGeometry().topLeft() +
+                     parent->window()->rect().center() - widget->rect().center());
+        return;
+    }
 
-        // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry()));
-        widget->move(findCenter(QGuiApplication::primaryScreen()->geometry()));
+    // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry()));
+    widget->move(findCenter(QGuiApplication::primaryScreen()->geometry()));
 }
 
 void
 utils::restoreCombobox(QComboBox *combo, const QString &value)
 {
-        for (auto i = 0; i < combo->count(); ++i) {
-                if (value == combo->itemText(i)) {
-                        combo->setCurrentIndex(i);
-                        break;
-                }
+    for (auto i = 0; i < combo->count(); ++i) {
+        if (value == combo->itemText(i)) {
+            combo->setCurrentIndex(i);
+            break;
         }
+    }
 }
 
 QImage
 utils::readImageFromFile(const QString &filename)
 {
-        QImageReader reader(filename);
-        reader.setAutoTransform(true);
-        return reader.read();
+    QImageReader reader(filename);
+    reader.setAutoTransform(true);
+    return reader.read();
 }
 QImage
 utils::readImage(const QByteArray &data)
 {
-        QBuffer buf;
-        buf.setData(data);
-        QImageReader reader(&buf);
-        reader.setAutoTransform(true);
-        return reader.read();
+    QBuffer buf;
+    buf.setData(data);
+    QImageReader reader(&buf);
+    reader.setAutoTransform(true);
+    return reader.read();
 }
 
 bool
 utils::isReply(const mtx::events::collections::TimelineEvents &e)
 {
-        return mtx::accessors::relations(e).reply_to().has_value();
+    return mtx::accessors::relations(e).reply_to().has_value();
 }
diff --git a/src/Utils.h b/src/Utils.h
index 8f37a5748b3fb11e0d359a21d70a4f50d90edbe6..da82ec7c04db4b0e69dae7514dc8c1d56fcb628e 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -29,12 +29,12 @@ class QComboBox;
 // outgoing messages
 struct RelatedInfo
 {
-        using MsgType = mtx::events::MessageType;
-        MsgType type;
-        QString room;
-        QString quoted_body, quoted_formatted_body;
-        std::string related_event;
-        QString quoted_user;
+    using MsgType = mtx::events::MessageType;
+    MsgType type;
+    QString room;
+    QString quoted_body, quoted_formatted_body;
+    std::string related_event;
+    QString quoted_user;
 };
 
 namespace utils {
@@ -97,112 +97,96 @@ messageDescription(const QString &username = "",
                    const QString &body     = "",
                    const bool isLocal      = false)
 {
-        using Audio      = mtx::events::RoomEvent<mtx::events::msg::Audio>;
-        using Emote      = mtx::events::RoomEvent<mtx::events::msg::Emote>;
-        using File       = mtx::events::RoomEvent<mtx::events::msg::File>;
-        using Image      = mtx::events::RoomEvent<mtx::events::msg::Image>;
-        using Notice     = mtx::events::RoomEvent<mtx::events::msg::Notice>;
-        using Sticker    = mtx::events::Sticker;
-        using Text       = mtx::events::RoomEvent<mtx::events::msg::Text>;
-        using Video      = mtx::events::RoomEvent<mtx::events::msg::Video>;
-        using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>;
-        using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>;
-        using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>;
-        using Encrypted  = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
-
-        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) {
-                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) {
-                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) {
-                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 if (std::is_same<T, CallInvite>::value) {
-                if (isLocal)
-                        return QCoreApplication::translate("message-description sent:",
-                                                           "You placed a call");
-                else
-                        return QCoreApplication::translate("message-description sent:",
-                                                           "%1 placed a call")
-                          .arg(username);
-        } else if (std::is_same<T, CallAnswer>::value) {
-                if (isLocal)
-                        return QCoreApplication::translate("message-description sent:",
-                                                           "You answered a call");
-                else
-                        return QCoreApplication::translate("message-description sent:",
-                                                           "%1 answered a call")
-                          .arg(username);
-        } else if (std::is_same<T, CallHangUp>::value) {
-                if (isLocal)
-                        return QCoreApplication::translate("message-description sent:",
-                                                           "You ended a call");
-                else
-                        return QCoreApplication::translate("message-description sent:",
-                                                           "%1 ended a call")
-                          .arg(username);
-        } else {
-                return QCoreApplication::translate("utils", "Unknown Message Type");
-        }
+    using Audio      = mtx::events::RoomEvent<mtx::events::msg::Audio>;
+    using Emote      = mtx::events::RoomEvent<mtx::events::msg::Emote>;
+    using File       = mtx::events::RoomEvent<mtx::events::msg::File>;
+    using Image      = mtx::events::RoomEvent<mtx::events::msg::Image>;
+    using Notice     = mtx::events::RoomEvent<mtx::events::msg::Notice>;
+    using Sticker    = mtx::events::Sticker;
+    using Text       = mtx::events::RoomEvent<mtx::events::msg::Text>;
+    using Video      = mtx::events::RoomEvent<mtx::events::msg::Video>;
+    using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>;
+    using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>;
+    using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>;
+    using Encrypted  = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
+
+    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) {
+        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) {
+        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) {
+        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 if (std::is_same<T, CallInvite>::value) {
+        if (isLocal)
+            return QCoreApplication::translate("message-description sent:", "You placed a call");
+        else
+            return QCoreApplication::translate("message-description sent:", "%1 placed a call")
+              .arg(username);
+    } else if (std::is_same<T, CallAnswer>::value) {
+        if (isLocal)
+            return QCoreApplication::translate("message-description sent:", "You answered a call");
+        else
+            return QCoreApplication::translate("message-description sent:", "%1 answered a call")
+              .arg(username);
+    } else if (std::is_same<T, CallHangUp>::value) {
+        if (isLocal)
+            return QCoreApplication::translate("message-description sent:", "You ended a call");
+        else
+            return QCoreApplication::translate("message-description sent:", "%1 ended a call")
+              .arg(username);
+    } else {
+        return QCoreApplication::translate("utils", "Unknown Message Type");
+    }
 }
 
 //! Scale down an image to fit to the given width & height limitations.
@@ -214,19 +198,19 @@ template<typename ContainerT, typename PredicateT>
 void
 erase_if(ContainerT &items, const PredicateT &predicate)
 {
-        for (auto it = items.begin(); it != items.end();) {
-                if (predicate(*it))
-                        it = items.erase(it);
-                else
-                        ++it;
-        }
+    for (auto it = items.begin(); it != items.end();) {
+        if (predicate(*it))
+            it = items.erase(it);
+        else
+            ++it;
+    }
 }
 
 template<class T>
 QString
 message_body(const mtx::events::collections::TimelineEvents &event)
 {
-        return QString::fromStdString(std::get<T>(event).content.body);
+    return QString::fromStdString(std::get<T>(event).content.body);
 }
 
 //! Calculate the Levenshtein distance between two strings with character skipping.
@@ -253,13 +237,13 @@ template<typename RoomMessageT>
 QString
 getMessageBody(const RoomMessageT &event)
 {
-        if (event.content.format.empty())
-                return QString::fromStdString(event.content.body).toHtmlEscaped();
+    if (event.content.format.empty())
+        return QString::fromStdString(event.content.body).toHtmlEscaped();
 
-        if (event.content.format != mtx::common::FORMAT_MSG_TYPE)
-                return QString::fromStdString(event.content.body).toHtmlEscaped();
+    if (event.content.format != mtx::common::FORMAT_MSG_TYPE)
+        return QString::fromStdString(event.content.body).toHtmlEscaped();
 
-        return QString::fromStdString(event.content.formatted_body);
+    return QString::fromStdString(event.content.formatted_body);
 }
 
 //! Replace raw URLs in text with HTML link tags.
diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp
deleted file mode 100644
index 723304356c9c4908d51cf2c47b357ecf362d404a..0000000000000000000000000000000000000000
--- a/src/WebRTCSession.cpp
+++ /dev/null
@@ -1,1185 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QQmlEngine>
-#include <QQuickItem>
-#include <algorithm>
-#include <cctype>
-#include <chrono>
-#include <cstdlib>
-#include <cstring>
-#include <optional>
-#include <string_view>
-#include <thread>
-#include <utility>
-
-#include "CallDevices.h"
-#include "ChatPage.h"
-#include "Logging.h"
-#include "UserSettingsPage.h"
-#include "WebRTCSession.h"
-
-#ifdef GSTREAMER_AVAILABLE
-extern "C"
-{
-#include "gst/gst.h"
-#include "gst/sdp/sdp.h"
-
-#define GST_USE_UNSTABLE_API
-#include "gst/webrtc/webrtc.h"
-}
-#endif
-
-// https://github.com/vector-im/riot-web/issues/10173
-#define STUN_SERVER "stun://turn.matrix.org:3478"
-
-Q_DECLARE_METATYPE(webrtc::CallType)
-Q_DECLARE_METATYPE(webrtc::State)
-
-using webrtc::CallType;
-using webrtc::State;
-
-WebRTCSession::WebRTCSession()
-  : devices_(CallDevices::instance())
-{
-        qRegisterMetaType<webrtc::CallType>();
-        qmlRegisterUncreatableMetaObject(
-          webrtc::staticMetaObject, "im.nheko", 1, 0, "CallType", "Can't instantiate enum");
-
-        qRegisterMetaType<webrtc::State>();
-        qmlRegisterUncreatableMetaObject(
-          webrtc::staticMetaObject, "im.nheko", 1, 0, "WebRTCState", "Can't instantiate enum");
-
-        connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
-        init();
-}
-
-bool
-WebRTCSession::init(std::string *errorMessage)
-{
-#ifdef GSTREAMER_AVAILABLE
-        if (initialised_)
-                return true;
-
-        GError *error = nullptr;
-        if (!gst_init_check(nullptr, nullptr, &error)) {
-                std::string strError("WebRTC: failed to initialise GStreamer: ");
-                if (error) {
-                        strError += error->message;
-                        g_error_free(error);
-                }
-                nhlog::ui()->error(strError);
-                if (errorMessage)
-                        *errorMessage = strError;
-                return false;
-        }
-
-        initialised_   = true;
-        gchar *version = gst_version_string();
-        nhlog::ui()->info("WebRTC: initialised {}", version);
-        g_free(version);
-        devices_.init();
-        return true;
-#else
-        (void)errorMessage;
-        return false;
-#endif
-}
-
-#ifdef GSTREAMER_AVAILABLE
-namespace {
-
-std::string localsdp_;
-std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
-bool haveAudioStream_     = false;
-bool haveVideoStream_     = false;
-GstPad *localPiPSinkPad_  = nullptr;
-GstPad *remotePiPSinkPad_ = nullptr;
-
-gboolean
-newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
-{
-        WebRTCSession *session = static_cast<WebRTCSession *>(user_data);
-        switch (GST_MESSAGE_TYPE(msg)) {
-        case GST_MESSAGE_EOS:
-                nhlog::ui()->error("WebRTC: end of stream");
-                session->end();
-                break;
-        case GST_MESSAGE_ERROR:
-                GError *error;
-                gchar *debug;
-                gst_message_parse_error(msg, &error, &debug);
-                nhlog::ui()->error(
-                  "WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message);
-                g_clear_error(&error);
-                g_free(debug);
-                session->end();
-                break;
-        default:
-                break;
-        }
-        return TRUE;
-}
-
-GstWebRTCSessionDescription *
-parseSDP(const std::string &sdp, GstWebRTCSDPType type)
-{
-        GstSDPMessage *msg;
-        gst_sdp_message_new(&msg);
-        if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) {
-                return gst_webrtc_session_description_new(type, msg);
-        } else {
-                nhlog::ui()->error("WebRTC: failed to parse remote session description");
-                gst_sdp_message_free(msg);
-                return nullptr;
-        }
-}
-
-void
-setLocalDescription(GstPromise *promise, gpointer webrtc)
-{
-        const GstStructure *reply = gst_promise_get_reply(promise);
-        gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer"));
-        GstWebRTCSessionDescription *gstsdp = nullptr;
-        gst_structure_get(reply,
-                          isAnswer ? "answer" : "offer",
-                          GST_TYPE_WEBRTC_SESSION_DESCRIPTION,
-                          &gstsdp,
-                          nullptr);
-        gst_promise_unref(promise);
-        g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr);
-
-        gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
-        localsdp_  = std::string(sdp);
-        g_free(sdp);
-        gst_webrtc_session_description_free(gstsdp);
-
-        nhlog::ui()->debug(
-          "WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_);
-}
-
-void
-createOffer(GstElement *webrtc)
-{
-        // create-offer first, then set-local-description
-        GstPromise *promise =
-          gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
-        g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise);
-}
-
-void
-createAnswer(GstPromise *promise, gpointer webrtc)
-{
-        // create-answer first, then set-local-description
-        gst_promise_unref(promise);
-        promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
-        g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise);
-}
-
-void
-iceGatheringStateChanged(GstElement *webrtc,
-                         GParamSpec *pspec G_GNUC_UNUSED,
-                         gpointer user_data G_GNUC_UNUSED)
-{
-        GstWebRTCICEGatheringState newState;
-        g_object_get(webrtc, "ice-gathering-state", &newState, nullptr);
-        if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) {
-                nhlog::ui()->debug("WebRTC: GstWebRTCICEGatheringState -> Complete");
-                if (WebRTCSession::instance().isOffering()) {
-                        emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_);
-                        emit WebRTCSession::instance().stateChanged(State::OFFERSENT);
-                } else {
-                        emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_);
-                        emit WebRTCSession::instance().stateChanged(State::ANSWERSENT);
-                }
-        }
-}
-
-void
-addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED,
-                     guint mlineIndex,
-                     gchar *candidate,
-                     gpointer G_GNUC_UNUSED)
-{
-        nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
-        localcandidates_.push_back({std::string() /*max-bundle*/, (uint16_t)mlineIndex, candidate});
-}
-
-void
-iceConnectionStateChanged(GstElement *webrtc,
-                          GParamSpec *pspec G_GNUC_UNUSED,
-                          gpointer user_data G_GNUC_UNUSED)
-{
-        GstWebRTCICEConnectionState newState;
-        g_object_get(webrtc, "ice-connection-state", &newState, nullptr);
-        switch (newState) {
-        case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING:
-                nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking");
-                emit WebRTCSession::instance().stateChanged(State::CONNECTING);
-                break;
-        case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED:
-                nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed");
-                emit WebRTCSession::instance().stateChanged(State::ICEFAILED);
-                break;
-        default:
-                break;
-        }
-}
-
-// https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1164
-struct KeyFrameRequestData
-{
-        GstElement *pipe      = nullptr;
-        GstElement *decodebin = nullptr;
-        gint packetsLost      = 0;
-        guint timerid         = 0;
-        std::string statsField;
-} keyFrameRequestData_;
-
-void
-sendKeyFrameRequest()
-{
-        GstPad *sinkpad = gst_element_get_static_pad(keyFrameRequestData_.decodebin, "sink");
-        if (!gst_pad_push_event(sinkpad,
-                                gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM,
-                                                     gst_structure_new_empty("GstForceKeyUnit"))))
-                nhlog::ui()->error("WebRTC: key frame request failed");
-        else
-                nhlog::ui()->debug("WebRTC: sent key frame request");
-
-        gst_object_unref(sinkpad);
-}
-
-void
-testPacketLoss_(GstPromise *promise, gpointer G_GNUC_UNUSED)
-{
-        const GstStructure *reply = gst_promise_get_reply(promise);
-        gint packetsLost          = 0;
-        GstStructure *rtpStats;
-        if (!gst_structure_get(reply,
-                               keyFrameRequestData_.statsField.c_str(),
-                               GST_TYPE_STRUCTURE,
-                               &rtpStats,
-                               nullptr)) {
-                nhlog::ui()->error("WebRTC: get-stats: no field: {}",
-                                   keyFrameRequestData_.statsField);
-                gst_promise_unref(promise);
-                return;
-        }
-        gst_structure_get_int(rtpStats, "packets-lost", &packetsLost);
-        gst_structure_free(rtpStats);
-        gst_promise_unref(promise);
-        if (packetsLost > keyFrameRequestData_.packetsLost) {
-                nhlog::ui()->debug("WebRTC: inbound video lost packet count: {}", packetsLost);
-                keyFrameRequestData_.packetsLost = packetsLost;
-                sendKeyFrameRequest();
-        }
-}
-
-gboolean
-testPacketLoss(gpointer G_GNUC_UNUSED)
-{
-        if (keyFrameRequestData_.pipe) {
-                GstElement *webrtc =
-                  gst_bin_get_by_name(GST_BIN(keyFrameRequestData_.pipe), "webrtcbin");
-                GstPromise *promise =
-                  gst_promise_new_with_change_func(testPacketLoss_, nullptr, nullptr);
-                g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise);
-                gst_object_unref(webrtc);
-                return TRUE;
-        }
-        return FALSE;
-}
-
-void
-setWaitForKeyFrame(GstBin *decodebin G_GNUC_UNUSED, GstElement *element, gpointer G_GNUC_UNUSED)
-{
-        if (!std::strcmp(
-              gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))),
-              "rtpvp8depay"))
-                g_object_set(element, "wait-for-keyframe", TRUE, nullptr);
-}
-
-GstElement *
-newAudioSinkChain(GstElement *pipe)
-{
-        GstElement *queue    = gst_element_factory_make("queue", nullptr);
-        GstElement *convert  = gst_element_factory_make("audioconvert", nullptr);
-        GstElement *resample = gst_element_factory_make("audioresample", nullptr);
-        GstElement *sink     = gst_element_factory_make("autoaudiosink", nullptr);
-        gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr);
-        gst_element_link_many(queue, convert, resample, sink, nullptr);
-        gst_element_sync_state_with_parent(queue);
-        gst_element_sync_state_with_parent(convert);
-        gst_element_sync_state_with_parent(resample);
-        gst_element_sync_state_with_parent(sink);
-        return queue;
-}
-
-GstElement *
-newVideoSinkChain(GstElement *pipe)
-{
-        // use compositor for now; acceleration needs investigation
-        GstElement *queue          = gst_element_factory_make("queue", nullptr);
-        GstElement *compositor     = gst_element_factory_make("compositor", "compositor");
-        GstElement *glupload       = gst_element_factory_make("glupload", nullptr);
-        GstElement *glcolorconvert = gst_element_factory_make("glcolorconvert", nullptr);
-        GstElement *qmlglsink      = gst_element_factory_make("qmlglsink", nullptr);
-        GstElement *glsinkbin      = gst_element_factory_make("glsinkbin", nullptr);
-        g_object_set(compositor, "background", 1, nullptr);
-        g_object_set(qmlglsink, "widget", WebRTCSession::instance().getVideoItem(), nullptr);
-        g_object_set(glsinkbin, "sink", qmlglsink, nullptr);
-        gst_bin_add_many(
-          GST_BIN(pipe), queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
-        gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
-        gst_element_sync_state_with_parent(queue);
-        gst_element_sync_state_with_parent(compositor);
-        gst_element_sync_state_with_parent(glupload);
-        gst_element_sync_state_with_parent(glcolorconvert);
-        gst_element_sync_state_with_parent(glsinkbin);
-        return queue;
-}
-
-std::pair<int, int>
-getResolution(GstPad *pad)
-{
-        std::pair<int, int> ret;
-        GstCaps *caps         = gst_pad_get_current_caps(pad);
-        const GstStructure *s = gst_caps_get_structure(caps, 0);
-        gst_structure_get_int(s, "width", &ret.first);
-        gst_structure_get_int(s, "height", &ret.second);
-        gst_caps_unref(caps);
-        return ret;
-}
-
-std::pair<int, int>
-getResolution(GstElement *pipe, const gchar *elementName, const gchar *padName)
-{
-        GstElement *element = gst_bin_get_by_name(GST_BIN(pipe), elementName);
-        GstPad *pad         = gst_element_get_static_pad(element, padName);
-        auto ret            = getResolution(pad);
-        gst_object_unref(pad);
-        gst_object_unref(element);
-        return ret;
-}
-
-std::pair<int, int>
-getPiPDimensions(const std::pair<int, int> &resolution, int fullWidth, double scaleFactor)
-{
-        int pipWidth  = fullWidth * scaleFactor;
-        int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth;
-        return {pipWidth, pipHeight};
-}
-
-void
-addLocalPiP(GstElement *pipe, const std::pair<int, int> &videoCallSize)
-{
-        // embed localUser's camera into received video (CallType::VIDEO)
-        // OR embed screen share into received video (CallType::SCREEN)
-        GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee");
-        if (!tee)
-                return;
-
-        GstElement *queue = gst_element_factory_make("queue", nullptr);
-        gst_bin_add(GST_BIN(pipe), queue);
-        gst_element_link(tee, queue);
-        gst_element_sync_state_with_parent(queue);
-        gst_object_unref(tee);
-
-        GstElement *compositor = gst_bin_get_by_name(GST_BIN(pipe), "compositor");
-        localPiPSinkPad_       = gst_element_get_request_pad(compositor, "sink_%u");
-        g_object_set(localPiPSinkPad_, "zorder", 2, nullptr);
-
-        bool isVideo         = WebRTCSession::instance().callType() == CallType::VIDEO;
-        const gchar *element = isVideo ? "camerafilter" : "screenshare";
-        const gchar *pad     = isVideo ? "sink" : "src";
-        auto resolution      = getResolution(pipe, element, pad);
-        auto pipSize         = getPiPDimensions(resolution, videoCallSize.first, 0.25);
-        nhlog::ui()->debug(
-          "WebRTC: local picture-in-picture: {}x{}", pipSize.first, pipSize.second);
-        g_object_set(localPiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr);
-        gint offset = videoCallSize.first / 80;
-        g_object_set(localPiPSinkPad_, "xpos", offset, "ypos", offset, nullptr);
-
-        GstPad *srcpad = gst_element_get_static_pad(queue, "src");
-        if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, localPiPSinkPad_)))
-                nhlog::ui()->error("WebRTC: failed to link local PiP elements");
-        gst_object_unref(srcpad);
-        gst_object_unref(compositor);
-}
-
-void
-addRemotePiP(GstElement *pipe)
-{
-        // embed localUser's camera into screen image being shared
-        if (remotePiPSinkPad_) {
-                auto camRes   = getResolution(pipe, "camerafilter", "sink");
-                auto shareRes = getResolution(pipe, "screenshare", "src");
-                auto pipSize  = getPiPDimensions(camRes, shareRes.first, 0.2);
-                nhlog::ui()->debug(
-                  "WebRTC: screen share picture-in-picture: {}x{}", pipSize.first, pipSize.second);
-
-                gint offset = shareRes.first / 100;
-                g_object_set(remotePiPSinkPad_, "zorder", 2, nullptr);
-                g_object_set(
-                  remotePiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr);
-                g_object_set(remotePiPSinkPad_,
-                             "xpos",
-                             shareRes.first - pipSize.first - offset,
-                             "ypos",
-                             shareRes.second - pipSize.second - offset,
-                             nullptr);
-        }
-}
-
-void
-addLocalVideo(GstElement *pipe)
-{
-        GstElement *queue = newVideoSinkChain(pipe);
-        GstElement *tee   = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee");
-        GstPad *srcpad    = gst_element_get_request_pad(tee, "src_%u");
-        GstPad *sinkpad   = gst_element_get_static_pad(queue, "sink");
-        if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, sinkpad)))
-                nhlog::ui()->error("WebRTC: failed to link videosrctee -> video sink chain");
-        gst_object_unref(srcpad);
-}
-
-void
-linkNewPad(GstElement *decodebin, GstPad *newpad, GstElement *pipe)
-{
-        GstPad *sinkpad               = gst_element_get_static_pad(decodebin, "sink");
-        GstCaps *sinkcaps             = gst_pad_get_current_caps(sinkpad);
-        const GstStructure *structure = gst_caps_get_structure(sinkcaps, 0);
-
-        gchar *mediaType = nullptr;
-        guint ssrc       = 0;
-        gst_structure_get(
-          structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr);
-        gst_caps_unref(sinkcaps);
-        gst_object_unref(sinkpad);
-
-        WebRTCSession *session = &WebRTCSession::instance();
-        GstElement *queue      = nullptr;
-        if (!std::strcmp(mediaType, "audio")) {
-                nhlog::ui()->debug("WebRTC: received incoming audio stream");
-                haveAudioStream_ = true;
-                queue            = newAudioSinkChain(pipe);
-        } else if (!std::strcmp(mediaType, "video")) {
-                nhlog::ui()->debug("WebRTC: received incoming video stream");
-                if (!session->getVideoItem()) {
-                        g_free(mediaType);
-                        nhlog::ui()->error("WebRTC: video call item not set");
-                        return;
-                }
-                haveVideoStream_ = true;
-                keyFrameRequestData_.statsField =
-                  std::string("rtp-inbound-stream-stats_") + std::to_string(ssrc);
-                queue              = newVideoSinkChain(pipe);
-                auto videoCallSize = getResolution(newpad);
-                nhlog::ui()->info("WebRTC: incoming video resolution: {}x{}",
-                                  videoCallSize.first,
-                                  videoCallSize.second);
-                addLocalPiP(pipe, videoCallSize);
-        } else {
-                g_free(mediaType);
-                nhlog::ui()->error("WebRTC: unknown pad type: {}", GST_PAD_NAME(newpad));
-                return;
-        }
-
-        GstPad *queuepad = gst_element_get_static_pad(queue, "sink");
-        if (queuepad) {
-                if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad)))
-                        nhlog::ui()->error("WebRTC: unable to link new pad");
-                else {
-                        if (session->callType() == CallType::VOICE ||
-                            (haveAudioStream_ &&
-                             (haveVideoStream_ || session->isRemoteVideoRecvOnly()))) {
-                                emit session->stateChanged(State::CONNECTED);
-                                if (haveVideoStream_) {
-                                        keyFrameRequestData_.pipe      = pipe;
-                                        keyFrameRequestData_.decodebin = decodebin;
-                                        keyFrameRequestData_.timerid =
-                                          g_timeout_add_seconds(3, testPacketLoss, nullptr);
-                                }
-                                addRemotePiP(pipe);
-                                if (session->isRemoteVideoRecvOnly())
-                                        addLocalVideo(pipe);
-                        }
-                }
-                gst_object_unref(queuepad);
-        }
-        g_free(mediaType);
-}
-
-void
-addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
-{
-        if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC)
-                return;
-
-        nhlog::ui()->debug("WebRTC: received incoming stream");
-        GstElement *decodebin = gst_element_factory_make("decodebin", nullptr);
-        // hardware decoding needs investigation; eg rendering fails if vaapi plugin installed
-        g_object_set(decodebin, "force-sw-decoders", TRUE, nullptr);
-        g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe);
-        g_signal_connect(decodebin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr);
-        gst_bin_add(GST_BIN(pipe), decodebin);
-        gst_element_sync_state_with_parent(decodebin);
-        GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink");
-        if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad)))
-                nhlog::ui()->error("WebRTC: unable to link decodebin");
-        gst_object_unref(sinkpad);
-}
-
-bool
-contains(std::string_view str1, std::string_view str2)
-{
-        return std::search(str1.cbegin(),
-                           str1.cend(),
-                           str2.cbegin(),
-                           str2.cend(),
-                           [](unsigned char c1, unsigned char c2) {
-                                   return std::tolower(c1) == std::tolower(c2);
-                           }) != str1.cend();
-}
-
-bool
-getMediaAttributes(const GstSDPMessage *sdp,
-                   const char *mediaType,
-                   const char *encoding,
-                   int &payloadType,
-                   bool &recvOnly,
-                   bool &sendOnly)
-{
-        payloadType = -1;
-        recvOnly    = false;
-        sendOnly    = false;
-        for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); ++mlineIndex) {
-                const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex);
-                if (!std::strcmp(gst_sdp_media_get_media(media), mediaType)) {
-                        recvOnly = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr;
-                        sendOnly = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr;
-                        const gchar *rtpval = nullptr;
-                        for (guint n = 0; n == 0 || rtpval; ++n) {
-                                rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n);
-                                if (rtpval && contains(rtpval, encoding)) {
-                                        payloadType = std::atoi(rtpval);
-                                        break;
-                                }
-                        }
-                        return true;
-                }
-        }
-        return false;
-}
-}
-
-bool
-WebRTCSession::havePlugins(bool isVideo, std::string *errorMessage)
-{
-        if (!initialised_ && !init(errorMessage))
-                return false;
-        if (!isVideo && haveVoicePlugins_)
-                return true;
-        if (isVideo && haveVideoPlugins_)
-                return true;
-
-        const gchar *voicePlugins[] = {"audioconvert",
-                                       "audioresample",
-                                       "autodetect",
-                                       "dtls",
-                                       "nice",
-                                       "opus",
-                                       "playback",
-                                       "rtpmanager",
-                                       "srtp",
-                                       "volume",
-                                       "webrtc",
-                                       nullptr};
-
-        const gchar *videoPlugins[] = {
-          "compositor", "opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr};
-
-        std::string strError("Missing GStreamer plugins: ");
-        const gchar **needed  = isVideo ? videoPlugins : voicePlugins;
-        bool &havePlugins     = isVideo ? haveVideoPlugins_ : haveVoicePlugins_;
-        havePlugins           = true;
-        GstRegistry *registry = gst_registry_get();
-        for (guint i = 0; i < g_strv_length((gchar **)needed); i++) {
-                GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]);
-                if (!plugin) {
-                        havePlugins = false;
-                        strError += std::string(needed[i]) + " ";
-                        continue;
-                }
-                gst_object_unref(plugin);
-        }
-        if (!havePlugins) {
-                nhlog::ui()->error(strError);
-                if (errorMessage)
-                        *errorMessage = strError;
-                return false;
-        }
-
-        if (isVideo) {
-                // load qmlglsink to register GStreamer's GstGLVideoItem QML type
-                GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr);
-                gst_object_unref(qmlglsink);
-        }
-        return true;
-}
-
-bool
-WebRTCSession::createOffer(CallType callType, uint32_t shareWindowId)
-{
-        clear();
-        isOffering_    = true;
-        callType_      = callType;
-        shareWindowId_ = shareWindowId;
-
-        // opus and vp8 rtp payload types must be defined dynamically
-        // therefore from the range [96-127]
-        // see for example https://tools.ietf.org/html/rfc7587
-        constexpr int opusPayloadType = 111;
-        constexpr int vp8PayloadType  = 96;
-        return startPipeline(opusPayloadType, vp8PayloadType);
-}
-
-bool
-WebRTCSession::acceptOffer(const std::string &sdp)
-{
-        nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp);
-        if (state_ != State::DISCONNECTED)
-                return false;
-
-        clear();
-        GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
-        if (!offer)
-                return false;
-
-        int opusPayloadType;
-        bool recvOnly;
-        bool sendOnly;
-        if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, recvOnly, sendOnly)) {
-                if (opusPayloadType == -1) {
-                        nhlog::ui()->error("WebRTC: remote audio offer - no opus encoding");
-                        gst_webrtc_session_description_free(offer);
-                        return false;
-                }
-        } else {
-                nhlog::ui()->error("WebRTC: remote offer - no audio media");
-                gst_webrtc_session_description_free(offer);
-                return false;
-        }
-
-        int vp8PayloadType;
-        bool isVideo = getMediaAttributes(offer->sdp,
-                                          "video",
-                                          "vp8",
-                                          vp8PayloadType,
-                                          isRemoteVideoRecvOnly_,
-                                          isRemoteVideoSendOnly_);
-        if (isVideo && vp8PayloadType == -1) {
-                nhlog::ui()->error("WebRTC: remote video offer - no vp8 encoding");
-                gst_webrtc_session_description_free(offer);
-                return false;
-        }
-        callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
-
-        if (!startPipeline(opusPayloadType, vp8PayloadType)) {
-                gst_webrtc_session_description_free(offer);
-                return false;
-        }
-
-        // avoid a race that sometimes leaves the generated answer without media tracks (a=ssrc
-        // lines)
-        std::this_thread::sleep_for(std::chrono::milliseconds(200));
-
-        // set-remote-description first, then create-answer
-        GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr);
-        g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise);
-        gst_webrtc_session_description_free(offer);
-        return true;
-}
-
-bool
-WebRTCSession::acceptAnswer(const std::string &sdp)
-{
-        nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp);
-        if (state_ != State::OFFERSENT)
-                return false;
-
-        GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
-        if (!answer) {
-                end();
-                return false;
-        }
-
-        if (callType_ != CallType::VOICE) {
-                int unused;
-                if (!getMediaAttributes(answer->sdp,
-                                        "video",
-                                        "vp8",
-                                        unused,
-                                        isRemoteVideoRecvOnly_,
-                                        isRemoteVideoSendOnly_))
-                        isRemoteVideoRecvOnly_ = true;
-        }
-
-        g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr);
-        gst_webrtc_session_description_free(answer);
-        return true;
-}
-
-void
-WebRTCSession::acceptICECandidates(
-  const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates)
-{
-        if (state_ >= State::INITIATED) {
-                for (const auto &c : candidates) {
-                        nhlog::ui()->debug(
-                          "WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate);
-                        if (!c.candidate.empty()) {
-                                g_signal_emit_by_name(webrtc_,
-                                                      "add-ice-candidate",
-                                                      c.sdpMLineIndex,
-                                                      c.candidate.c_str());
-                        }
-                }
-        }
-}
-
-bool
-WebRTCSession::startPipeline(int opusPayloadType, int vp8PayloadType)
-{
-        if (state_ != State::DISCONNECTED)
-                return false;
-
-        emit stateChanged(State::INITIATING);
-
-        if (!createPipeline(opusPayloadType, vp8PayloadType)) {
-                end();
-                return false;
-        }
-
-        webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin");
-
-        if (ChatPage::instance()->userSettings()->useStunServer()) {
-                nhlog::ui()->info("WebRTC: setting STUN server: {}", STUN_SERVER);
-                g_object_set(webrtc_, "stun-server", STUN_SERVER, nullptr);
-        }
-
-        for (const auto &uri : turnServers_) {
-                nhlog::ui()->info("WebRTC: setting TURN server: {}", uri);
-                gboolean udata;
-                g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata));
-        }
-        if (turnServers_.empty())
-                nhlog::ui()->warn("WebRTC: no TURN server provided");
-
-        // generate the offer when the pipeline goes to PLAYING
-        if (isOffering_)
-                g_signal_connect(
-                  webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr);
-
-        // on-ice-candidate is emitted when a local ICE candidate has been gathered
-        g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr);
-
-        // capture ICE failure
-        g_signal_connect(
-          webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr);
-
-        // incoming streams trigger pad-added
-        gst_element_set_state(pipe_, GST_STATE_READY);
-        g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_);
-
-        // capture ICE gathering completion
-        g_signal_connect(
-          webrtc_, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), nullptr);
-
-        // webrtcbin lifetime is the same as that of the pipeline
-        gst_object_unref(webrtc_);
-
-        // start the pipeline
-        GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING);
-        if (ret == GST_STATE_CHANGE_FAILURE) {
-                nhlog::ui()->error("WebRTC: unable to start pipeline");
-                end();
-                return false;
-        }
-
-        GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
-        busWatchId_ = gst_bus_add_watch(bus, newBusMessage, this);
-        gst_object_unref(bus);
-        emit stateChanged(State::INITIATED);
-        return true;
-}
-
-bool
-WebRTCSession::createPipeline(int opusPayloadType, int vp8PayloadType)
-{
-        GstDevice *device = devices_.audioDevice();
-        if (!device)
-                return false;
-
-        GstElement *source     = gst_device_create_element(device, nullptr);
-        GstElement *volume     = gst_element_factory_make("volume", "srclevel");
-        GstElement *convert    = gst_element_factory_make("audioconvert", nullptr);
-        GstElement *resample   = gst_element_factory_make("audioresample", nullptr);
-        GstElement *queue1     = gst_element_factory_make("queue", nullptr);
-        GstElement *opusenc    = gst_element_factory_make("opusenc", nullptr);
-        GstElement *rtp        = gst_element_factory_make("rtpopuspay", nullptr);
-        GstElement *queue2     = gst_element_factory_make("queue", nullptr);
-        GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr);
-
-        GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp",
-                                               "media",
-                                               G_TYPE_STRING,
-                                               "audio",
-                                               "encoding-name",
-                                               G_TYPE_STRING,
-                                               "OPUS",
-                                               "payload",
-                                               G_TYPE_INT,
-                                               opusPayloadType,
-                                               nullptr);
-        g_object_set(capsfilter, "caps", rtpcaps, nullptr);
-        gst_caps_unref(rtpcaps);
-
-        GstElement *webrtcbin = gst_element_factory_make("webrtcbin", "webrtcbin");
-        g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr);
-
-        pipe_ = gst_pipeline_new(nullptr);
-        gst_bin_add_many(GST_BIN(pipe_),
-                         source,
-                         volume,
-                         convert,
-                         resample,
-                         queue1,
-                         opusenc,
-                         rtp,
-                         queue2,
-                         capsfilter,
-                         webrtcbin,
-                         nullptr);
-
-        if (!gst_element_link_many(source,
-                                   volume,
-                                   convert,
-                                   resample,
-                                   queue1,
-                                   opusenc,
-                                   rtp,
-                                   queue2,
-                                   capsfilter,
-                                   webrtcbin,
-                                   nullptr)) {
-                nhlog::ui()->error("WebRTC: failed to link audio pipeline elements");
-                return false;
-        }
-
-        return callType_ == CallType::VOICE || isRemoteVideoSendOnly_
-                 ? true
-                 : addVideoPipeline(vp8PayloadType);
-}
-
-bool
-WebRTCSession::addVideoPipeline(int vp8PayloadType)
-{
-        // allow incoming video calls despite localUser having no webcam
-        if (callType_ == CallType::VIDEO && !devices_.haveCamera())
-                return !isOffering_;
-
-        auto settings            = ChatPage::instance()->userSettings();
-        GstElement *camerafilter = nullptr;
-        GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
-        GstElement *tee          = gst_element_factory_make("tee", "videosrctee");
-        gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr);
-        if (callType_ == CallType::VIDEO || (settings->screenSharePiP() && devices_.haveCamera())) {
-                std::pair<int, int> resolution;
-                std::pair<int, int> frameRate;
-                GstDevice *device = devices_.videoDevice(resolution, frameRate);
-                if (!device)
-                        return false;
-
-                GstElement *camera = gst_device_create_element(device, nullptr);
-                GstCaps *caps      = gst_caps_new_simple("video/x-raw",
-                                                    "width",
-                                                    G_TYPE_INT,
-                                                    resolution.first,
-                                                    "height",
-                                                    G_TYPE_INT,
-                                                    resolution.second,
-                                                    "framerate",
-                                                    GST_TYPE_FRACTION,
-                                                    frameRate.first,
-                                                    frameRate.second,
-                                                    nullptr);
-                camerafilter       = gst_element_factory_make("capsfilter", "camerafilter");
-                g_object_set(camerafilter, "caps", caps, nullptr);
-                gst_caps_unref(caps);
-
-                gst_bin_add_many(GST_BIN(pipe_), camera, camerafilter, nullptr);
-                if (!gst_element_link_many(camera, videoconvert, camerafilter, nullptr)) {
-                        nhlog::ui()->error("WebRTC: failed to link camera elements");
-                        return false;
-                }
-                if (callType_ == CallType::VIDEO && !gst_element_link(camerafilter, tee)) {
-                        nhlog::ui()->error("WebRTC: failed to link camerafilter -> tee");
-                        return false;
-                }
-        }
-
-        if (callType_ == CallType::SCREEN) {
-                nhlog::ui()->debug("WebRTC: screen share frame rate: {} fps",
-                                   settings->screenShareFrameRate());
-                nhlog::ui()->debug("WebRTC: screen share picture-in-picture: {}",
-                                   settings->screenSharePiP());
-                nhlog::ui()->debug("WebRTC: screen share request remote camera: {}",
-                                   settings->screenShareRemoteVideo());
-                nhlog::ui()->debug("WebRTC: screen share hide mouse cursor: {}",
-                                   settings->screenShareHideCursor());
-
-                GstElement *ximagesrc = gst_element_factory_make("ximagesrc", "screenshare");
-                if (!ximagesrc) {
-                        nhlog::ui()->error("WebRTC: failed to create ximagesrc");
-                        return false;
-                }
-                g_object_set(ximagesrc, "use-damage", FALSE, nullptr);
-                g_object_set(ximagesrc, "xid", shareWindowId_, nullptr);
-                g_object_set(
-                  ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr);
-
-                GstCaps *caps          = gst_caps_new_simple("video/x-raw",
-                                                    "framerate",
-                                                    GST_TYPE_FRACTION,
-                                                    settings->screenShareFrameRate(),
-                                                    1,
-                                                    nullptr);
-                GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr);
-                g_object_set(capsfilter, "caps", caps, nullptr);
-                gst_caps_unref(caps);
-                gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr);
-
-                if (settings->screenSharePiP() && devices_.haveCamera()) {
-                        GstElement *compositor = gst_element_factory_make("compositor", nullptr);
-                        g_object_set(compositor, "background", 1, nullptr);
-                        gst_bin_add(GST_BIN(pipe_), compositor);
-                        if (!gst_element_link_many(
-                              ximagesrc, compositor, capsfilter, tee, nullptr)) {
-                                nhlog::ui()->error("WebRTC: failed to link screen share elements");
-                                return false;
-                        }
-
-                        GstPad *srcpad    = gst_element_get_static_pad(camerafilter, "src");
-                        remotePiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u");
-                        if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, remotePiPSinkPad_))) {
-                                nhlog::ui()->error(
-                                  "WebRTC: failed to link camerafilter -> compositor");
-                                gst_object_unref(srcpad);
-                                return false;
-                        }
-                        gst_object_unref(srcpad);
-                } else if (!gst_element_link_many(
-                             ximagesrc, videoconvert, capsfilter, tee, nullptr)) {
-                        nhlog::ui()->error("WebRTC: failed to link screen share elements");
-                        return false;
-                }
-        }
-
-        GstElement *queue  = gst_element_factory_make("queue", nullptr);
-        GstElement *vp8enc = gst_element_factory_make("vp8enc", nullptr);
-        g_object_set(vp8enc, "deadline", 1, nullptr);
-        g_object_set(vp8enc, "error-resilient", 1, nullptr);
-        GstElement *rtpvp8pay     = gst_element_factory_make("rtpvp8pay", nullptr);
-        GstElement *rtpqueue      = gst_element_factory_make("queue", nullptr);
-        GstElement *rtpcapsfilter = gst_element_factory_make("capsfilter", nullptr);
-        GstCaps *rtpcaps          = gst_caps_new_simple("application/x-rtp",
-                                               "media",
-                                               G_TYPE_STRING,
-                                               "video",
-                                               "encoding-name",
-                                               G_TYPE_STRING,
-                                               "VP8",
-                                               "payload",
-                                               G_TYPE_INT,
-                                               vp8PayloadType,
-                                               nullptr);
-        g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr);
-        gst_caps_unref(rtpcaps);
-
-        gst_bin_add_many(
-          GST_BIN(pipe_), queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, nullptr);
-
-        GstElement *webrtcbin = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin");
-        if (!gst_element_link_many(
-              tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) {
-                nhlog::ui()->error("WebRTC: failed to link rtp video elements");
-                gst_object_unref(webrtcbin);
-                return false;
-        }
-
-        if (callType_ == CallType::SCREEN &&
-            !ChatPage::instance()->userSettings()->screenShareRemoteVideo()) {
-                GArray *transceivers;
-                g_signal_emit_by_name(webrtcbin, "get-transceivers", &transceivers);
-                GstWebRTCRTPTransceiver *transceiver =
-                  g_array_index(transceivers, GstWebRTCRTPTransceiver *, 1);
-                transceiver->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY;
-                g_array_unref(transceivers);
-        }
-
-        gst_object_unref(webrtcbin);
-        return true;
-}
-
-bool
-WebRTCSession::haveLocalPiP() const
-{
-        if (state_ >= State::INITIATED) {
-                if (callType_ == CallType::VOICE || isRemoteVideoRecvOnly_)
-                        return false;
-                else if (callType_ == CallType::SCREEN)
-                        return true;
-                else {
-                        GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe_), "videosrctee");
-                        if (tee) {
-                                gst_object_unref(tee);
-                                return true;
-                        }
-                }
-        }
-        return false;
-}
-
-bool
-WebRTCSession::isMicMuted() const
-{
-        if (state_ < State::INITIATED)
-                return false;
-
-        GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
-        gboolean muted;
-        g_object_get(srclevel, "mute", &muted, nullptr);
-        gst_object_unref(srclevel);
-        return muted;
-}
-
-bool
-WebRTCSession::toggleMicMute()
-{
-        if (state_ < State::INITIATED)
-                return false;
-
-        GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
-        gboolean muted;
-        g_object_get(srclevel, "mute", &muted, nullptr);
-        g_object_set(srclevel, "mute", !muted, nullptr);
-        gst_object_unref(srclevel);
-        return !muted;
-}
-
-void
-WebRTCSession::toggleLocalPiP()
-{
-        if (localPiPSinkPad_) {
-                guint zorder;
-                g_object_get(localPiPSinkPad_, "zorder", &zorder, nullptr);
-                g_object_set(localPiPSinkPad_, "zorder", zorder ? 0 : 2, nullptr);
-        }
-}
-
-void
-WebRTCSession::clear()
-{
-        callType_              = webrtc::CallType::VOICE;
-        isOffering_            = false;
-        isRemoteVideoRecvOnly_ = false;
-        isRemoteVideoSendOnly_ = false;
-        videoItem_             = nullptr;
-        pipe_                  = nullptr;
-        webrtc_                = nullptr;
-        busWatchId_            = 0;
-        shareWindowId_         = 0;
-        haveAudioStream_       = false;
-        haveVideoStream_       = false;
-        localPiPSinkPad_       = nullptr;
-        remotePiPSinkPad_      = nullptr;
-        localsdp_.clear();
-        localcandidates_.clear();
-}
-
-void
-WebRTCSession::end()
-{
-        nhlog::ui()->debug("WebRTC: ending session");
-        keyFrameRequestData_ = KeyFrameRequestData{};
-        if (pipe_) {
-                gst_element_set_state(pipe_, GST_STATE_NULL);
-                gst_object_unref(pipe_);
-                pipe_ = nullptr;
-                if (busWatchId_) {
-                        g_source_remove(busWatchId_);
-                        busWatchId_ = 0;
-                }
-        }
-
-        clear();
-        if (state_ != State::DISCONNECTED)
-                emit stateChanged(State::DISCONNECTED);
-}
-
-#else
-
-bool
-WebRTCSession::havePlugins(bool, std::string *)
-{
-        return false;
-}
-
-bool
-WebRTCSession::haveLocalPiP() const
-{
-        return false;
-}
-
-bool WebRTCSession::createOffer(webrtc::CallType, uint32_t) { return false; }
-
-bool
-WebRTCSession::acceptOffer(const std::string &)
-{
-        return false;
-}
-
-bool
-WebRTCSession::acceptAnswer(const std::string &)
-{
-        return false;
-}
-
-void
-WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &)
-{}
-
-bool
-WebRTCSession::isMicMuted() const
-{
-        return false;
-}
-
-bool
-WebRTCSession::toggleMicMute()
-{
-        return false;
-}
-
-void
-WebRTCSession::toggleLocalPiP()
-{}
-
-void
-WebRTCSession::end()
-{}
-
-#endif
diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h
deleted file mode 100644
index 97487c5c988207d3b2dda137c0438891d3ac2623..0000000000000000000000000000000000000000
--- a/src/WebRTCSession.h
+++ /dev/null
@@ -1,117 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <string>
-#include <vector>
-
-#include <QObject>
-
-#include "mtx/events/voip.hpp"
-
-typedef struct _GstElement GstElement;
-class CallDevices;
-class QQuickItem;
-
-namespace webrtc {
-Q_NAMESPACE
-
-enum class CallType
-{
-        VOICE,
-        VIDEO,
-        SCREEN // localUser is sharing screen
-};
-Q_ENUM_NS(CallType)
-
-enum class State
-{
-        DISCONNECTED,
-        ICEFAILED,
-        INITIATING,
-        INITIATED,
-        OFFERSENT,
-        ANSWERSENT,
-        CONNECTING,
-        CONNECTED
-
-};
-Q_ENUM_NS(State)
-}
-
-class WebRTCSession : public QObject
-{
-        Q_OBJECT
-
-public:
-        static WebRTCSession &instance()
-        {
-                static WebRTCSession instance;
-                return instance;
-        }
-
-        bool havePlugins(bool isVideo, std::string *errorMessage = nullptr);
-        webrtc::CallType callType() const { return callType_; }
-        webrtc::State state() const { return state_; }
-        bool haveLocalPiP() const;
-        bool isOffering() const { return isOffering_; }
-        bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; }
-        bool isRemoteVideoSendOnly() const { return isRemoteVideoSendOnly_; }
-
-        bool createOffer(webrtc::CallType, uint32_t shareWindowId);
-        bool acceptOffer(const std::string &sdp);
-        bool acceptAnswer(const std::string &sdp);
-        void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
-
-        bool isMicMuted() const;
-        bool toggleMicMute();
-        void toggleLocalPiP();
-        void end();
-
-        void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; }
-
-        void setVideoItem(QQuickItem *item) { videoItem_ = item; }
-        QQuickItem *getVideoItem() const { return videoItem_; }
-
-signals:
-        void offerCreated(const std::string &sdp,
-                          const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
-        void answerCreated(const std::string &sdp,
-                           const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
-        void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &);
-        void stateChanged(webrtc::State);
-
-private slots:
-        void setState(webrtc::State state) { state_ = state; }
-
-private:
-        WebRTCSession();
-
-        CallDevices &devices_;
-        bool initialised_           = false;
-        bool haveVoicePlugins_      = false;
-        bool haveVideoPlugins_      = false;
-        webrtc::CallType callType_  = webrtc::CallType::VOICE;
-        webrtc::State state_        = webrtc::State::DISCONNECTED;
-        bool isOffering_            = false;
-        bool isRemoteVideoRecvOnly_ = false;
-        bool isRemoteVideoSendOnly_ = false;
-        QQuickItem *videoItem_      = nullptr;
-        GstElement *pipe_           = nullptr;
-        GstElement *webrtc_         = nullptr;
-        unsigned int busWatchId_    = 0;
-        std::vector<std::string> turnServers_;
-        uint32_t shareWindowId_ = 0;
-
-        bool init(std::string *errorMessage = nullptr);
-        bool startPipeline(int opusPayloadType, int vp8PayloadType);
-        bool createPipeline(int opusPayloadType, int vp8PayloadType);
-        bool addVideoPipeline(int vp8PayloadType);
-        void clear();
-
-public:
-        WebRTCSession(WebRTCSession const &) = delete;
-        void operator=(WebRTCSession const &) = delete;
-};
diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp
index 2cce7b8dc9dfdf5c6e6e945e492657d56e71777d..c7168789a5ad8c456b3e33bfc7868bb2d35b1630 100644
--- a/src/WelcomePage.cpp
+++ b/src/WelcomePage.cpp
@@ -16,72 +16,72 @@
 WelcomePage::WelcomePage(QWidget *parent)
   : QWidget(parent)
 {
-        auto topLayout_ = new QVBoxLayout(this);
-        topLayout_->setSpacing(20);
-        topLayout_->setAlignment(Qt::AlignCenter);
+    auto topLayout_ = new QVBoxLayout(this);
+    topLayout_->setSpacing(20);
+    topLayout_->setAlignment(Qt::AlignCenter);
 
-        QFont headingFont;
-        headingFont.setPointSizeF(headingFont.pointSizeF() * 2);
-        QFont subTitleFont;
-        subTitleFont.setPointSizeF(subTitleFont.pointSizeF() * 1.5);
+    QFont headingFont;
+    headingFont.setPointSizeF(headingFont.pointSizeF() * 2);
+    QFont subTitleFont;
+    subTitleFont.setPointSizeF(subTitleFont.pointSizeF() * 1.5);
 
-        QIcon icon{QIcon::fromTheme("nheko", QIcon{":/logos/splash.png"})};
+    QIcon icon{QIcon::fromTheme("nheko", QIcon{":/logos/splash.png"})};
 
-        auto logo_ = new QLabel(this);
-        logo_->setPixmap(icon.pixmap(256));
-        logo_->setAlignment(Qt::AlignCenter);
+    auto logo_ = new QLabel(this);
+    logo_->setPixmap(icon.pixmap(256));
+    logo_->setAlignment(Qt::AlignCenter);
 
-        QString heading(tr("Welcome to nheko! The desktop client for the Matrix protocol."));
-        QString main(tr("Enjoy your stay!"));
+    QString heading(tr("Welcome to nheko! The desktop client for the Matrix protocol."));
+    QString main(tr("Enjoy your stay!"));
 
-        auto intoTxt_ = new TextLabel(heading, this);
-        intoTxt_->setFont(headingFont);
-        intoTxt_->setAlignment(Qt::AlignCenter);
+    auto intoTxt_ = new TextLabel(heading, this);
+    intoTxt_->setFont(headingFont);
+    intoTxt_->setAlignment(Qt::AlignCenter);
 
-        auto subTitle = new TextLabel(main, this);
-        subTitle->setFont(subTitleFont);
-        subTitle->setAlignment(Qt::AlignCenter);
+    auto subTitle = new TextLabel(main, this);
+    subTitle->setFont(subTitleFont);
+    subTitle->setAlignment(Qt::AlignCenter);
 
-        topLayout_->addStretch(1);
-        topLayout_->addWidget(logo_);
-        topLayout_->addWidget(intoTxt_);
-        topLayout_->addWidget(subTitle);
+    topLayout_->addStretch(1);
+    topLayout_->addWidget(logo_);
+    topLayout_->addWidget(intoTxt_);
+    topLayout_->addWidget(subTitle);
 
-        auto btnLayout_ = new QHBoxLayout();
-        btnLayout_->setSpacing(20);
-        btnLayout_->setContentsMargins(0, 20, 0, 20);
+    auto btnLayout_ = new QHBoxLayout();
+    btnLayout_->setSpacing(20);
+    btnLayout_->setContentsMargins(0, 20, 0, 20);
 
-        const int fontHeight   = QFontMetrics{subTitleFont}.height();
-        const int buttonHeight = fontHeight * 2.5;
-        const int buttonWidth  = fontHeight * 8;
+    const int fontHeight   = QFontMetrics{subTitleFont}.height();
+    const int buttonHeight = fontHeight * 2.5;
+    const int buttonWidth  = fontHeight * 8;
 
-        auto registerBtn = new RaisedButton(tr("REGISTER"), this);
-        registerBtn->setMinimumSize(buttonWidth, buttonHeight);
-        registerBtn->setFontSize(subTitleFont.pointSizeF());
-        registerBtn->setCornerRadius(conf::btn::cornerRadius);
+    auto registerBtn = new RaisedButton(tr("REGISTER"), this);
+    registerBtn->setMinimumSize(buttonWidth, buttonHeight);
+    registerBtn->setFontSize(subTitleFont.pointSizeF());
+    registerBtn->setCornerRadius(conf::btn::cornerRadius);
 
-        auto loginBtn = new RaisedButton(tr("LOGIN"), this);
-        loginBtn->setMinimumSize(buttonWidth, buttonHeight);
-        loginBtn->setFontSize(subTitleFont.pointSizeF());
-        loginBtn->setCornerRadius(conf::btn::cornerRadius);
+    auto loginBtn = new RaisedButton(tr("LOGIN"), this);
+    loginBtn->setMinimumSize(buttonWidth, buttonHeight);
+    loginBtn->setFontSize(subTitleFont.pointSizeF());
+    loginBtn->setCornerRadius(conf::btn::cornerRadius);
 
-        btnLayout_->addStretch(1);
-        btnLayout_->addWidget(registerBtn);
-        btnLayout_->addWidget(loginBtn);
-        btnLayout_->addStretch(1);
+    btnLayout_->addStretch(1);
+    btnLayout_->addWidget(registerBtn);
+    btnLayout_->addWidget(loginBtn);
+    btnLayout_->addStretch(1);
 
-        topLayout_->addLayout(btnLayout_);
-        topLayout_->addStretch(1);
+    topLayout_->addLayout(btnLayout_);
+    topLayout_->addStretch(1);
 
-        connect(registerBtn, &QPushButton::clicked, this, &WelcomePage::userRegister);
-        connect(loginBtn, &QPushButton::clicked, this, &WelcomePage::userLogin);
+    connect(registerBtn, &QPushButton::clicked, this, &WelcomePage::userRegister);
+    connect(loginBtn, &QPushButton::clicked, this, &WelcomePage::userLogin);
 }
 
 void
 WelcomePage::paintEvent(QPaintEvent *)
 {
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+    QStyleOption opt;
+    opt.init(this);
+    QPainter p(this);
+    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
 }
diff --git a/src/WelcomePage.h b/src/WelcomePage.h
index d2dcc0c99b7430a092c3f15956bf597a1b649bc9..aa531a03b820b637d93a35530f240dffe049acb8 100644
--- a/src/WelcomePage.h
+++ b/src/WelcomePage.h
@@ -8,18 +8,18 @@
 
 class WelcomePage : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        explicit WelcomePage(QWidget *parent = nullptr);
+    explicit WelcomePage(QWidget *parent = nullptr);
 
 protected:
-        void paintEvent(QPaintEvent *) override;
+    void paintEvent(QPaintEvent *) override;
 
 signals:
-        // Notify that the user wants to login in.
-        void userLogin();
+    // Notify that the user wants to login in.
+    void userLogin();
 
-        // Notify that the user wants to register.
-        void userRegister();
+    // Notify that the user wants to register.
+    void userRegister();
 };
diff --git a/src/dialogs/CreateRoom.cpp b/src/dialogs/CreateRoom.cpp
index ba38543692afd166b240d4b68a3f03116167daa3..30dbf83ddc5d9afbeb08a64ae9719ea758a6bb25 100644
--- a/src/dialogs/CreateRoom.cpp
+++ b/src/dialogs/CreateRoom.cpp
@@ -18,142 +18,142 @@ using namespace dialogs;
 CreateRoom::CreateRoom(QWidget *parent)
   : QFrame(parent)
 {
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-
-        QFont largeFont;
-        largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
-
-        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-        setMinimumHeight(conf::modals::MIN_WIDGET_HEIGHT);
-        setMinimumWidth(conf::window::minModalWidth);
-
-        auto layout = new QVBoxLayout(this);
-        layout->setSpacing(conf::modals::WIDGET_SPACING);
-        layout->setMargin(conf::modals::WIDGET_MARGIN);
-
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(15);
-
-        confirmBtn_ = new QPushButton(tr("Create room"), this);
-        confirmBtn_->setDefault(true);
-        cancelBtn_ = new QPushButton(tr("Cancel"), this);
-
-        buttonLayout->addStretch(1);
-        buttonLayout->addWidget(cancelBtn_);
-        buttonLayout->addWidget(confirmBtn_);
-
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * 1.3);
-
-        nameInput_ = new TextField(this);
-        nameInput_->setLabel(tr("Name"));
-
-        topicInput_ = new TextField(this);
-        topicInput_->setLabel(tr("Topic"));
-
-        aliasInput_ = new TextField(this);
-        aliasInput_->setLabel(tr("Alias"));
-
-        auto visibilityLayout = new QHBoxLayout;
-        visibilityLayout->setContentsMargins(0, 10, 0, 10);
-
-        auto presetLayout = new QHBoxLayout;
-        presetLayout->setContentsMargins(0, 10, 0, 10);
-
-        auto visibilityLabel = new QLabel(tr("Room Visibility"), this);
-        visibilityCombo_     = new QComboBox(this);
-        visibilityCombo_->addItem("Private");
-        visibilityCombo_->addItem("Public");
-
-        visibilityLayout->addWidget(visibilityLabel);
-        visibilityLayout->addWidget(visibilityCombo_, 0, Qt::AlignBottom | Qt::AlignRight);
-
-        auto presetLabel = new QLabel(tr("Room Preset"), this);
-        presetCombo_     = new QComboBox(this);
-        presetCombo_->addItem("Private Chat");
-        presetCombo_->addItem("Public Chat");
-        presetCombo_->addItem("Trusted Private Chat");
-
-        presetLayout->addWidget(presetLabel);
-        presetLayout->addWidget(presetCombo_, 0, Qt::AlignBottom | Qt::AlignRight);
-
-        auto directLabel_ = new QLabel(tr("Direct Chat"), this);
-        directToggle_     = new Toggle(this);
-        directToggle_->setActiveColor(QColor("#38A3D8"));
-        directToggle_->setInactiveColor(QColor("gray"));
-        directToggle_->setState(false);
-
-        auto directLayout = new QHBoxLayout;
-        directLayout->setContentsMargins(0, 10, 0, 10);
-        directLayout->addWidget(directLabel_);
-        directLayout->addWidget(directToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
-
-        layout->addWidget(nameInput_);
-        layout->addWidget(topicInput_);
-        layout->addWidget(aliasInput_);
-        layout->addLayout(visibilityLayout);
-        layout->addLayout(presetLayout);
-        layout->addLayout(directLayout);
-        layout->addLayout(buttonLayout);
-
-        connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
-                request_.name            = nameInput_->text().toStdString();
-                request_.topic           = topicInput_->text().toStdString();
-                request_.room_alias_name = aliasInput_->text().toStdString();
-
-                emit createRoom(request_);
-
-                clearFields();
-                emit close();
-        });
-
-        connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
-                clearFields();
-                emit close();
-        });
-
-        connect(visibilityCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &text) {
-                        if (text == "Private") {
-                                request_.visibility = mtx::common::RoomVisibility::Private;
-                        } else {
-                                request_.visibility = mtx::common::RoomVisibility::Public;
-                        }
-                });
-
-        connect(presetCombo_,
-                static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
-                [this](const QString &text) {
-                        if (text == "Private Chat") {
-                                request_.preset = mtx::requests::Preset::PrivateChat;
-                        } else if (text == "Public Chat") {
-                                request_.preset = mtx::requests::Preset::PublicChat;
-                        } else {
-                                request_.preset = mtx::requests::Preset::TrustedPrivateChat;
-                        }
-                });
-
-        connect(directToggle_, &Toggle::toggled, this, [this](bool isEnabled) {
-                request_.is_direct = isEnabled;
-        });
+    setAutoFillBackground(true);
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+    setWindowModality(Qt::WindowModal);
+    setAttribute(Qt::WA_DeleteOnClose, true);
+
+    QFont largeFont;
+    largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
+
+    setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+    setMinimumHeight(conf::modals::MIN_WIDGET_HEIGHT);
+    setMinimumWidth(conf::window::minModalWidth);
+
+    auto layout = new QVBoxLayout(this);
+    layout->setSpacing(conf::modals::WIDGET_SPACING);
+    layout->setMargin(conf::modals::WIDGET_MARGIN);
+
+    auto buttonLayout = new QHBoxLayout();
+    buttonLayout->setSpacing(15);
+
+    confirmBtn_ = new QPushButton(tr("Create room"), this);
+    confirmBtn_->setDefault(true);
+    cancelBtn_ = new QPushButton(tr("Cancel"), this);
+
+    buttonLayout->addStretch(1);
+    buttonLayout->addWidget(cancelBtn_);
+    buttonLayout->addWidget(confirmBtn_);
+
+    QFont font;
+    font.setPointSizeF(font.pointSizeF() * 1.3);
+
+    nameInput_ = new TextField(this);
+    nameInput_->setLabel(tr("Name"));
+
+    topicInput_ = new TextField(this);
+    topicInput_->setLabel(tr("Topic"));
+
+    aliasInput_ = new TextField(this);
+    aliasInput_->setLabel(tr("Alias"));
+
+    auto visibilityLayout = new QHBoxLayout;
+    visibilityLayout->setContentsMargins(0, 10, 0, 10);
+
+    auto presetLayout = new QHBoxLayout;
+    presetLayout->setContentsMargins(0, 10, 0, 10);
+
+    auto visibilityLabel = new QLabel(tr("Room Visibility"), this);
+    visibilityCombo_     = new QComboBox(this);
+    visibilityCombo_->addItem("Private");
+    visibilityCombo_->addItem("Public");
+
+    visibilityLayout->addWidget(visibilityLabel);
+    visibilityLayout->addWidget(visibilityCombo_, 0, Qt::AlignBottom | Qt::AlignRight);
+
+    auto presetLabel = new QLabel(tr("Room Preset"), this);
+    presetCombo_     = new QComboBox(this);
+    presetCombo_->addItem("Private Chat");
+    presetCombo_->addItem("Public Chat");
+    presetCombo_->addItem("Trusted Private Chat");
+
+    presetLayout->addWidget(presetLabel);
+    presetLayout->addWidget(presetCombo_, 0, Qt::AlignBottom | Qt::AlignRight);
+
+    auto directLabel_ = new QLabel(tr("Direct Chat"), this);
+    directToggle_     = new Toggle(this);
+    directToggle_->setActiveColor(QColor("#38A3D8"));
+    directToggle_->setInactiveColor(QColor("gray"));
+    directToggle_->setState(false);
+
+    auto directLayout = new QHBoxLayout;
+    directLayout->setContentsMargins(0, 10, 0, 10);
+    directLayout->addWidget(directLabel_);
+    directLayout->addWidget(directToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
+
+    layout->addWidget(nameInput_);
+    layout->addWidget(topicInput_);
+    layout->addWidget(aliasInput_);
+    layout->addLayout(visibilityLayout);
+    layout->addLayout(presetLayout);
+    layout->addLayout(directLayout);
+    layout->addLayout(buttonLayout);
+
+    connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
+        request_.name            = nameInput_->text().toStdString();
+        request_.topic           = topicInput_->text().toStdString();
+        request_.room_alias_name = aliasInput_->text().toStdString();
+
+        emit createRoom(request_);
+
+        clearFields();
+        emit close();
+    });
+
+    connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
+        clearFields();
+        emit close();
+    });
+
+    connect(visibilityCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &text) {
+                if (text == "Private") {
+                    request_.visibility = mtx::common::RoomVisibility::Private;
+                } else {
+                    request_.visibility = mtx::common::RoomVisibility::Public;
+                }
+            });
+
+    connect(presetCombo_,
+            static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
+            [this](const QString &text) {
+                if (text == "Private Chat") {
+                    request_.preset = mtx::requests::Preset::PrivateChat;
+                } else if (text == "Public Chat") {
+                    request_.preset = mtx::requests::Preset::PublicChat;
+                } else {
+                    request_.preset = mtx::requests::Preset::TrustedPrivateChat;
+                }
+            });
+
+    connect(directToggle_, &Toggle::toggled, this, [this](bool isEnabled) {
+        request_.is_direct = isEnabled;
+    });
 }
 
 void
 CreateRoom::clearFields()
 {
-        nameInput_->clear();
-        topicInput_->clear();
-        aliasInput_->clear();
+    nameInput_->clear();
+    topicInput_->clear();
+    aliasInput_->clear();
 }
 
 void
 CreateRoom::showEvent(QShowEvent *event)
 {
-        nameInput_->setFocus();
+    nameInput_->setFocus();
 
-        QFrame::showEvent(event);
+    QFrame::showEvent(event);
 }
diff --git a/src/dialogs/CreateRoom.h b/src/dialogs/CreateRoom.h
index d4c6474d16a145f1e3b7e10dcb3e42e37ef8bf40..d9d90a10a112a4c3e823206cd7cd45242476fd60 100644
--- a/src/dialogs/CreateRoom.h
+++ b/src/dialogs/CreateRoom.h
@@ -17,32 +17,32 @@ namespace dialogs {
 
 class CreateRoom : public QFrame
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        CreateRoom(QWidget *parent = nullptr);
+    CreateRoom(QWidget *parent = nullptr);
 
 signals:
-        void createRoom(const mtx::requests::CreateRoom &request);
+    void createRoom(const mtx::requests::CreateRoom &request);
 
 protected:
-        void showEvent(QShowEvent *event) override;
+    void showEvent(QShowEvent *event) override;
 
 private:
-        void clearFields();
+    void clearFields();
 
-        QComboBox *visibilityCombo_;
-        QComboBox *presetCombo_;
+    QComboBox *visibilityCombo_;
+    QComboBox *presetCombo_;
 
-        Toggle *directToggle_;
+    Toggle *directToggle_;
 
-        QPushButton *confirmBtn_;
-        QPushButton *cancelBtn_;
+    QPushButton *confirmBtn_;
+    QPushButton *cancelBtn_;
 
-        TextField *nameInput_;
-        TextField *topicInput_;
-        TextField *aliasInput_;
+    TextField *nameInput_;
+    TextField *topicInput_;
+    TextField *aliasInput_;
 
-        mtx::requests::CreateRoom request_;
+    mtx::requests::CreateRoom request_;
 };
 
 } // dialogs
diff --git a/src/dialogs/FallbackAuth.cpp b/src/dialogs/FallbackAuth.cpp
index c7b179f431030a23362e154a23faf6e53326313e..2b8dfed9582d3ccf5d63d56484074ff7fe7638e5 100644
--- a/src/dialogs/FallbackAuth.cpp
+++ b/src/dialogs/FallbackAuth.cpp
@@ -18,56 +18,56 @@ using namespace dialogs;
 FallbackAuth::FallbackAuth(const QString &authType, const QString &session, QWidget *parent)
   : QWidget(parent)
 {
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
+    setAutoFillBackground(true);
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+    setWindowModality(Qt::WindowModal);
+    setAttribute(Qt::WA_DeleteOnClose, true);
 
-        auto layout = new QVBoxLayout(this);
-        layout->setSpacing(conf::modals::WIDGET_SPACING);
-        layout->setMargin(conf::modals::WIDGET_MARGIN);
+    auto layout = new QVBoxLayout(this);
+    layout->setSpacing(conf::modals::WIDGET_SPACING);
+    layout->setMargin(conf::modals::WIDGET_MARGIN);
 
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(8);
-        buttonLayout->setMargin(0);
+    auto buttonLayout = new QHBoxLayout();
+    buttonLayout->setSpacing(8);
+    buttonLayout->setMargin(0);
 
-        openBtn_    = new QPushButton(tr("Open Fallback in Browser"), this);
-        cancelBtn_  = new QPushButton(tr("Cancel"), this);
-        confirmBtn_ = new QPushButton(tr("Confirm"), this);
-        confirmBtn_->setDefault(true);
+    openBtn_    = new QPushButton(tr("Open Fallback in Browser"), this);
+    cancelBtn_  = new QPushButton(tr("Cancel"), this);
+    confirmBtn_ = new QPushButton(tr("Confirm"), this);
+    confirmBtn_->setDefault(true);
 
-        buttonLayout->addStretch(1);
-        buttonLayout->addWidget(openBtn_);
-        buttonLayout->addWidget(cancelBtn_);
-        buttonLayout->addWidget(confirmBtn_);
+    buttonLayout->addStretch(1);
+    buttonLayout->addWidget(openBtn_);
+    buttonLayout->addWidget(cancelBtn_);
+    buttonLayout->addWidget(confirmBtn_);
 
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
+    QFont font;
+    font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
 
-        auto label = new QLabel(
-          tr("Open the fallback, follow the steps and confirm after completing them."), this);
-        label->setFont(font);
+    auto label = new QLabel(
+      tr("Open the fallback, follow the steps and confirm after completing them."), this);
+    label->setFont(font);
 
-        layout->addWidget(label);
-        layout->addLayout(buttonLayout);
+    layout->addWidget(label);
+    layout->addLayout(buttonLayout);
 
-        connect(openBtn_, &QPushButton::clicked, [session, authType]() {
-                const auto url = QString("https://%1:%2/_matrix/client/r0/auth/%4/"
-                                         "fallback/web?session=%3")
-                                   .arg(QString::fromStdString(http::client()->server()))
-                                   .arg(http::client()->port())
-                                   .arg(session)
-                                   .arg(authType);
+    connect(openBtn_, &QPushButton::clicked, [session, authType]() {
+        const auto url = QString("https://%1:%2/_matrix/client/r0/auth/%4/"
+                                 "fallback/web?session=%3")
+                           .arg(QString::fromStdString(http::client()->server()))
+                           .arg(http::client()->port())
+                           .arg(session)
+                           .arg(authType);
 
-                QDesktopServices::openUrl(url);
-        });
+        QDesktopServices::openUrl(url);
+    });
 
-        connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
-                emit confirmation();
-                emit close();
-        });
-        connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
-                emit cancel();
-                emit close();
-        });
+    connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
+        emit confirmation();
+        emit close();
+    });
+    connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
+        emit cancel();
+        emit close();
+    });
 }
diff --git a/src/dialogs/FallbackAuth.h b/src/dialogs/FallbackAuth.h
index 8e4e28eadb31836417b2a3e773e5bfb78f019b9a..6bfd59f70430c077c4107bc7b4cb0032e057ce07 100644
--- a/src/dialogs/FallbackAuth.h
+++ b/src/dialogs/FallbackAuth.h
@@ -13,18 +13,18 @@ namespace dialogs {
 
 class FallbackAuth : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        FallbackAuth(const QString &authType, const QString &session, QWidget *parent = nullptr);
+    FallbackAuth(const QString &authType, const QString &session, QWidget *parent = nullptr);
 
 signals:
-        void confirmation();
-        void cancel();
+    void confirmation();
+    void cancel();
 
 private:
-        QPushButton *openBtn_;
-        QPushButton *confirmBtn_;
-        QPushButton *cancelBtn_;
+    QPushButton *openBtn_;
+    QPushButton *confirmBtn_;
+    QPushButton *cancelBtn_;
 };
 } // dialogs
diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index 12813d57551e9b1e46eacbff89b5c7ea01b87b3c..8c90a744563f437d522b96998d956c7e4c5245ef 100644
--- a/src/dialogs/ImageOverlay.cpp
+++ b/src/dialogs/ImageOverlay.cpp
@@ -19,84 +19,83 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
   : QWidget{parent}
   , originalImage_{image}
 {
-        setMouseTracking(true);
-        setParent(nullptr);
+    setMouseTracking(true);
+    setParent(nullptr);
 
-        setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
+    setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
 
-        setAttribute(Qt::WA_NoSystemBackground, true);
-        setAttribute(Qt::WA_TranslucentBackground, true);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-        setWindowState(Qt::WindowFullScreen);
-        close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this);
+    setAttribute(Qt::WA_NoSystemBackground, true);
+    setAttribute(Qt::WA_TranslucentBackground, true);
+    setAttribute(Qt::WA_DeleteOnClose, true);
+    setWindowState(Qt::WindowFullScreen);
+    close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this);
 
-        connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing);
-        connect(this, &ImageOverlay::closing, this, &ImageOverlay::close);
+    connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing);
+    connect(this, &ImageOverlay::closing, this, &ImageOverlay::close);
 
-        raise();
+    raise();
 }
 
 void
 ImageOverlay::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event);
+    Q_UNUSED(event);
 
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
 
-        // Full screen overlay.
-        painter.fillRect(QRect(0, 0, width(), height()), QColor(55, 55, 55, 170));
+    // Full screen overlay.
+    painter.fillRect(QRect(0, 0, width(), height()), QColor(55, 55, 55, 170));
 
-        // Left and Right margins
-        int outer_margin = width() * 0.12;
-        int buttonSize   = 36;
-        int margin       = outer_margin * 0.1;
+    // Left and Right margins
+    int outer_margin = width() * 0.12;
+    int buttonSize   = 36;
+    int margin       = outer_margin * 0.1;
 
-        int max_width  = width() - 2 * outer_margin;
-        int max_height = height();
+    int max_width  = width() - 2 * outer_margin;
+    int max_height = height();
 
-        image_ = utils::scaleDown(max_width, max_height, originalImage_);
+    image_ = utils::scaleDown(max_width, max_height, originalImage_);
 
-        int diff_x = max_width - image_.width();
-        int diff_y = max_height - image_.height();
+    int diff_x = max_width - image_.width();
+    int diff_y = max_height - image_.height();
 
-        content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height());
-        close_button_ = QRect(width() - margin - buttonSize, margin, buttonSize, buttonSize);
-        save_button_ =
-          QRect(width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize);
+    content_      = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height());
+    close_button_ = QRect(width() - margin - buttonSize, margin, buttonSize, buttonSize);
+    save_button_ = QRect(width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize);
 
-        // Draw main content_.
-        painter.drawPixmap(content_, image_);
+    // Draw main content_.
+    painter.drawPixmap(content_, image_);
 
-        // Draw top right corner X.
-        QPen pen;
-        pen.setCapStyle(Qt::RoundCap);
-        pen.setWidthF(5);
-        pen.setColor("gray");
+    // Draw top right corner X.
+    QPen pen;
+    pen.setCapStyle(Qt::RoundCap);
+    pen.setWidthF(5);
+    pen.setColor("gray");
 
-        auto center = close_button_.center();
+    auto center = close_button_.center();
 
-        painter.setPen(pen);
-        painter.drawLine(center - QPointF(15, 15), center + QPointF(15, 15));
-        painter.drawLine(center + QPointF(15, -15), center - QPointF(15, -15));
+    painter.setPen(pen);
+    painter.drawLine(center - QPointF(15, 15), center + QPointF(15, 15));
+    painter.drawLine(center + QPointF(15, -15), center - QPointF(15, -15));
 
-        // Draw download button
-        center = save_button_.center();
-        painter.drawLine(center - QPointF(0, 15), center + QPointF(0, 15));
-        painter.drawLine(center - QPointF(15, 0), center + QPointF(0, 15));
-        painter.drawLine(center + QPointF(0, 15), center + QPointF(15, 0));
+    // Draw download button
+    center = save_button_.center();
+    painter.drawLine(center - QPointF(0, 15), center + QPointF(0, 15));
+    painter.drawLine(center - QPointF(15, 0), center + QPointF(0, 15));
+    painter.drawLine(center + QPointF(0, 15), center + QPointF(15, 0));
 }
 
 void
 ImageOverlay::mousePressEvent(QMouseEvent *event)
 {
-        if (event->button() != Qt::LeftButton)
-                return;
-
-        if (close_button_.contains(event->pos()))
-                emit closing();
-        else if (save_button_.contains(event->pos()))
-                emit saving();
-        else if (!content_.contains(event->pos()))
-                emit closing();
+    if (event->button() != Qt::LeftButton)
+        return;
+
+    if (close_button_.contains(event->pos()))
+        emit closing();
+    else if (save_button_.contains(event->pos()))
+        emit saving();
+    else if (!content_.contains(event->pos()))
+        emit closing();
 }
diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h
index 9d4187bf83c29b32c937cfdf58c89baa78bb8827..2174279f761668fcaa8517003629a9868b86f317 100644
--- a/src/dialogs/ImageOverlay.h
+++ b/src/dialogs/ImageOverlay.h
@@ -14,25 +14,25 @@ namespace dialogs {
 
 class ImageOverlay : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        ImageOverlay(QPixmap image, QWidget *parent = nullptr);
+    ImageOverlay(QPixmap image, QWidget *parent = nullptr);
 
 protected:
-        void mousePressEvent(QMouseEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
+    void mousePressEvent(QMouseEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 signals:
-        void closing();
-        void saving();
+    void closing();
+    void saving();
 
 private:
-        QPixmap originalImage_;
-        QPixmap image_;
+    QPixmap originalImage_;
+    QPixmap image_;
 
-        QRect content_;
-        QRect close_button_;
-        QRect save_button_;
-        QShortcut *close_shortcut_;
+    QRect content_;
+    QRect close_button_;
+    QRect save_button_;
+    QShortcut *close_shortcut_;
 };
 } // dialogs
diff --git a/src/dialogs/JoinRoom.cpp b/src/dialogs/JoinRoom.cpp
deleted file mode 100644
index dc2e48046d3055dfc56d8e651ee97bc3564d0db4..0000000000000000000000000000000000000000
--- a/src/dialogs/JoinRoom.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QLabel>
-#include <QPushButton>
-#include <QVBoxLayout>
-
-#include "dialogs/JoinRoom.h"
-
-#include "Config.h"
-#include "ui/TextField.h"
-
-using namespace dialogs;
-
-JoinRoom::JoinRoom(QWidget *parent)
-  : QFrame(parent)
-{
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-
-        setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
-        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
-        auto layout = new QVBoxLayout(this);
-        layout->setSpacing(conf::modals::WIDGET_SPACING);
-        layout->setMargin(conf::modals::WIDGET_MARGIN);
-
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(15);
-
-        confirmBtn_ = new QPushButton(tr("Join"), this);
-        confirmBtn_->setDefault(true);
-        cancelBtn_ = new QPushButton(tr("Cancel"), this);
-
-        buttonLayout->addStretch(1);
-        buttonLayout->addWidget(cancelBtn_);
-        buttonLayout->addWidget(confirmBtn_);
-
-        roomInput_ = new TextField(this);
-        roomInput_->setLabel(tr("Room ID or alias"));
-
-        layout->addWidget(roomInput_);
-        layout->addLayout(buttonLayout);
-        layout->addStretch(1);
-
-        connect(roomInput_, &QLineEdit::returnPressed, this, &JoinRoom::handleInput);
-        connect(confirmBtn_, &QPushButton::clicked, this, &JoinRoom::handleInput);
-        connect(cancelBtn_, &QPushButton::clicked, this, &JoinRoom::close);
-}
-
-void
-JoinRoom::handleInput()
-{
-        if (roomInput_->text().isEmpty())
-                return;
-
-        // TODO: input validation with error messages.
-        emit joinRoom(roomInput_->text());
-        roomInput_->clear();
-
-        emit close();
-}
-
-void
-JoinRoom::showEvent(QShowEvent *event)
-{
-        roomInput_->setFocus();
-
-        QFrame::showEvent(event);
-}
diff --git a/src/dialogs/JoinRoom.h b/src/dialogs/JoinRoom.h
deleted file mode 100644
index f399f1fb7c20817dc29d60e10f77c5f934d9af32..0000000000000000000000000000000000000000
--- a/src/dialogs/JoinRoom.h
+++ /dev/null
@@ -1,36 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFrame>
-
-class QPushButton;
-class TextField;
-
-namespace dialogs {
-
-class JoinRoom : public QFrame
-{
-        Q_OBJECT
-public:
-        JoinRoom(QWidget *parent = nullptr);
-
-signals:
-        void joinRoom(const QString &room);
-
-protected:
-        void showEvent(QShowEvent *event) override;
-
-private slots:
-        void handleInput();
-
-private:
-        QPushButton *confirmBtn_;
-        QPushButton *cancelBtn_;
-
-        TextField *roomInput_;
-};
-
-} // dialogs
diff --git a/src/dialogs/LeaveRoom.cpp b/src/dialogs/LeaveRoom.cpp
deleted file mode 100644
index 5246d6934afda2746f3c423586bda393004b11d3..0000000000000000000000000000000000000000
--- a/src/dialogs/LeaveRoom.cpp
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QLabel>
-#include <QPushButton>
-#include <QVBoxLayout>
-
-#include "dialogs/LeaveRoom.h"
-
-#include "Config.h"
-
-using namespace dialogs;
-
-LeaveRoom::LeaveRoom(QWidget *parent)
-  : QFrame(parent)
-{
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-
-        setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
-        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
-        auto layout = new QVBoxLayout(this);
-        layout->setSpacing(conf::modals::WIDGET_SPACING);
-        layout->setMargin(conf::modals::WIDGET_MARGIN);
-
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(0);
-        buttonLayout->setMargin(0);
-
-        confirmBtn_ = new QPushButton("Leave", this);
-        cancelBtn_  = new QPushButton(tr("Cancel"), this);
-        cancelBtn_->setDefault(true);
-
-        buttonLayout->addStretch(1);
-        buttonLayout->setSpacing(15);
-        buttonLayout->addWidget(cancelBtn_);
-        buttonLayout->addWidget(confirmBtn_);
-
-        auto label = new QLabel(tr("Are you sure you want to leave?"), this);
-
-        layout->addWidget(label);
-        layout->addLayout(buttonLayout);
-
-        connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
-                emit leaving();
-                emit close();
-        });
-        connect(cancelBtn_, &QPushButton::clicked, this, &LeaveRoom::close);
-}
diff --git a/src/dialogs/LeaveRoom.h b/src/dialogs/LeaveRoom.h
deleted file mode 100644
index e94655794a8ad935af5c4dca71a273173690b4df..0000000000000000000000000000000000000000
--- a/src/dialogs/LeaveRoom.h
+++ /dev/null
@@ -1,26 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFrame>
-
-class QPushButton;
-
-namespace dialogs {
-
-class LeaveRoom : public QFrame
-{
-        Q_OBJECT
-public:
-        explicit LeaveRoom(QWidget *parent = nullptr);
-
-signals:
-        void leaving();
-
-private:
-        QPushButton *confirmBtn_;
-        QPushButton *cancelBtn_;
-};
-} // dialogs
diff --git a/src/dialogs/Logout.cpp b/src/dialogs/Logout.cpp
index fdfc3338141acfa75815e6f8e555ad7da3ed91ca..d10e4cdfb32ef4a817754ccf621d81115abb91d2 100644
--- a/src/dialogs/Logout.cpp
+++ b/src/dialogs/Logout.cpp
@@ -15,40 +15,40 @@ using namespace dialogs;
 Logout::Logout(QWidget *parent)
   : QFrame(parent)
 {
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-
-        setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
-        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
-        auto layout = new QVBoxLayout(this);
-        layout->setSpacing(conf::modals::WIDGET_SPACING);
-        layout->setMargin(conf::modals::WIDGET_MARGIN);
-
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(0);
-        buttonLayout->setMargin(0);
-
-        confirmBtn_ = new QPushButton("Logout", this);
-        cancelBtn_  = new QPushButton(tr("Cancel"), this);
-        cancelBtn_->setDefault(true);
-
-        buttonLayout->addStretch(1);
-        buttonLayout->setSpacing(15);
-        buttonLayout->addWidget(cancelBtn_);
-        buttonLayout->addWidget(confirmBtn_);
-
-        auto label = new QLabel(tr("Logout. Are you sure?"), this);
-
-        layout->addWidget(label);
-        layout->addLayout(buttonLayout);
-        layout->addStretch(1);
-
-        connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
-                emit loggingOut();
-                emit close();
-        });
-        connect(cancelBtn_, &QPushButton::clicked, this, &Logout::close);
+    setAutoFillBackground(true);
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+    setWindowModality(Qt::WindowModal);
+    setAttribute(Qt::WA_DeleteOnClose, true);
+
+    setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
+    setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+    auto layout = new QVBoxLayout(this);
+    layout->setSpacing(conf::modals::WIDGET_SPACING);
+    layout->setMargin(conf::modals::WIDGET_MARGIN);
+
+    auto buttonLayout = new QHBoxLayout();
+    buttonLayout->setSpacing(0);
+    buttonLayout->setMargin(0);
+
+    confirmBtn_ = new QPushButton("Logout", this);
+    cancelBtn_  = new QPushButton(tr("Cancel"), this);
+    cancelBtn_->setDefault(true);
+
+    buttonLayout->addStretch(1);
+    buttonLayout->setSpacing(15);
+    buttonLayout->addWidget(cancelBtn_);
+    buttonLayout->addWidget(confirmBtn_);
+
+    auto label = new QLabel(tr("Logout. Are you sure?"), this);
+
+    layout->addWidget(label);
+    layout->addLayout(buttonLayout);
+    layout->addStretch(1);
+
+    connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
+        emit loggingOut();
+        emit close();
+    });
+    connect(cancelBtn_, &QPushButton::clicked, this, &Logout::close);
 }
diff --git a/src/dialogs/Logout.h b/src/dialogs/Logout.h
index 9d8d0f4ba9461f50b33ea30b156d7274b591934c..7783c68f21bc2799e8a0898540a3ce2a4bafd66f 100644
--- a/src/dialogs/Logout.h
+++ b/src/dialogs/Logout.h
@@ -13,15 +13,15 @@ namespace dialogs {
 
 class Logout : public QFrame
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        explicit Logout(QWidget *parent = nullptr);
+    explicit Logout(QWidget *parent = nullptr);
 
 signals:
-        void loggingOut();
+    void loggingOut();
 
 private:
-        QPushButton *confirmBtn_;
-        QPushButton *cancelBtn_;
+    QPushButton *confirmBtn_;
+    QPushButton *cancelBtn_;
 };
 } // dialogs
diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp
index 66fa1b3774062599ce9d2348b4140575296b68cc..2e95bd9113ecaf2d3af101526eaf36445ff5da19 100644
--- a/src/dialogs/PreviewUploadOverlay.cpp
+++ b/src/dialogs/PreviewUploadOverlay.cpp
@@ -29,188 +29,192 @@ PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
   , upload_{tr("Upload"), this}
   , cancel_{tr("Cancel"), this}
 {
-        auto hlayout = new QHBoxLayout;
-        hlayout->addStretch(1);
-        hlayout->addWidget(&cancel_);
-        hlayout->addWidget(&upload_);
-        hlayout->setMargin(0);
-
-        auto vlayout = new QVBoxLayout{this};
-        vlayout->addWidget(&titleLabel_);
-        vlayout->addWidget(&infoLabel_);
-        vlayout->addWidget(&fileName_);
-        vlayout->addLayout(hlayout);
-        vlayout->setSpacing(conf::modals::WIDGET_SPACING);
-        vlayout->setMargin(conf::modals::WIDGET_MARGIN);
-
-        upload_.setDefault(true);
-        connect(&upload_, &QPushButton::clicked, [this]() {
-                emit confirmUpload(data_, mediaType_, fileName_.text());
-                close();
-        });
-
-        connect(&fileName_, &QLineEdit::returnPressed, this, [this]() {
-                emit confirmUpload(data_, mediaType_, fileName_.text());
-                close();
-        });
-
-        connect(&cancel_, &QPushButton::clicked, this, [this]() {
-                emit aborted();
-                close();
-        });
+    auto hlayout = new QHBoxLayout;
+    hlayout->addStretch(1);
+    hlayout->addWidget(&cancel_);
+    hlayout->addWidget(&upload_);
+    hlayout->setMargin(0);
+
+    auto vlayout = new QVBoxLayout{this};
+    vlayout->addWidget(&titleLabel_);
+    vlayout->addWidget(&infoLabel_);
+    vlayout->addWidget(&fileName_);
+    vlayout->addLayout(hlayout);
+    vlayout->setSpacing(conf::modals::WIDGET_SPACING);
+    vlayout->setMargin(conf::modals::WIDGET_MARGIN);
+
+    upload_.setDefault(true);
+    connect(&upload_, &QPushButton::clicked, [this]() {
+        emit confirmUpload(data_, mediaType_, fileName_.text());
+        close();
+    });
+
+    connect(&fileName_, &QLineEdit::returnPressed, this, [this]() {
+        emit confirmUpload(data_, mediaType_, fileName_.text());
+        close();
+    });
+
+    connect(&cancel_, &QPushButton::clicked, this, [this]() {
+        emit aborted();
+        close();
+    });
 }
 
 void
 PreviewUploadOverlay::init()
 {
-        QSize winsize;
-        QPoint center;
-
-        auto window = MainWindow::instance();
-        if (window) {
-                winsize = window->frameGeometry().size();
-                center  = window->frameGeometry().center();
-        } else {
-                nhlog::ui()->warn("unable to retrieve MainWindow's size");
-        }
-
-        fileName_.setText(QFileInfo{filePath_}.fileName());
-
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
-
-        titleLabel_.setFont(font);
-        titleLabel_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
-        titleLabel_.setAlignment(Qt::AlignCenter);
-        infoLabel_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-        fileName_.setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
-        fileName_.setAlignment(Qt::AlignCenter);
-        upload_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
-        cancel_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
-
-        if (isImage_) {
-                infoLabel_.setAlignment(Qt::AlignCenter);
-
-                const auto maxWidth  = winsize.width() * 0.8;
-                const auto maxHeight = winsize.height() * 0.8;
-
-                // Scale image preview to fit into the application window.
-                infoLabel_.setPixmap(utils::scaleDown(maxWidth, maxHeight, image_));
-                move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
-        } else {
-                infoLabel_.setAlignment(Qt::AlignLeft);
-        }
-        infoLabel_.setScaledContents(false);
-
-        show();
+    QSize winsize;
+    QPoint center;
+
+    auto window = MainWindow::instance();
+    if (window) {
+        winsize = window->frameGeometry().size();
+        center  = window->frameGeometry().center();
+    } else {
+        nhlog::ui()->warn("unable to retrieve MainWindow's size");
+    }
+
+    fileName_.setText(QFileInfo{filePath_}.fileName());
+
+    setAutoFillBackground(true);
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+    setWindowModality(Qt::WindowModal);
+
+    QFont font;
+    font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
+
+    titleLabel_.setFont(font);
+    titleLabel_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+    titleLabel_.setAlignment(Qt::AlignCenter);
+    infoLabel_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+    fileName_.setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+    fileName_.setAlignment(Qt::AlignCenter);
+    upload_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+    cancel_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+
+    if (isImage_) {
+        infoLabel_.setAlignment(Qt::AlignCenter);
+
+        const auto maxWidth  = winsize.width() * 0.8;
+        const auto maxHeight = winsize.height() * 0.8;
+
+        // Scale image preview to fit into the application window.
+        infoLabel_.setPixmap(utils::scaleDown(maxWidth, maxHeight, image_));
+        move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
+    } else {
+        infoLabel_.setAlignment(Qt::AlignLeft);
+    }
+    infoLabel_.setScaledContents(false);
+
+    show();
 }
 
 void
 PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64_t upload_size)
 {
-        if (mediaType_.split('/')[0] == "image") {
-                if (!image_.loadFromData(data_)) {
-                        titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
-                } else {
-                        titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_));
-                }
-                isImage_ = true;
+    if (mediaType_.split('/')[0] == "image") {
+        if (!image_.loadFromData(data_)) {
+            titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
         } else {
-                auto const info = QString{tr("Media type: %1\n"
-                                             "Media size: %2\n")}
-                                    .arg(mime)
-                                    .arg(utils::humanReadableFileSize(upload_size));
-
-                titleLabel_.setText(QString{tr(DEFAULT)}.arg("file"));
-                infoLabel_.setText(info);
+            titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_));
         }
+        isImage_ = true;
+    } else {
+        auto const info = QString{tr("Media type: %1\n"
+                                     "Media size: %2\n")}
+                            .arg(mime)
+                            .arg(utils::humanReadableFileSize(upload_size));
+
+        titleLabel_.setText(QString{tr(DEFAULT)}.arg("file"));
+        infoLabel_.setText(info);
+    }
 }
 
 void
 PreviewUploadOverlay::setPreview(const QImage &src, const QString &mime)
 {
-        nhlog::ui()->info("Pasting image with size: {}x{}, format: {}",
-                          src.height(),
-                          src.width(),
-                          mime.toStdString());
-
-        auto const &split = mime.split('/');
-        auto const &type  = split[1];
-
-        QBuffer buffer(&data_);
-        buffer.open(QIODevice::WriteOnly);
-        if (src.save(&buffer, type.toStdString().c_str()))
-                titleLabel_.setText(QString{tr(DEFAULT)}.arg("image"));
-        else
-                titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
-
-        mediaType_ = mime;
-        filePath_  = "clipboard." + type;
-        image_.convertFromImage(src);
-        isImage_ = true;
+    nhlog::ui()->info(
+      "Pasting image with size: {}x{}, format: {}", src.height(), src.width(), mime.toStdString());
 
+    auto const &split = mime.split('/');
+    auto const &type  = split[1];
+
+    QBuffer buffer(&data_);
+    buffer.open(QIODevice::WriteOnly);
+    if (src.save(&buffer, type.toStdString().c_str()))
         titleLabel_.setText(QString{tr(DEFAULT)}.arg("image"));
-        init();
+    else
+        titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
+
+    mediaType_ = mime;
+    filePath_  = "clipboard." + type;
+    image_.convertFromImage(src);
+    isImage_ = true;
+
+    titleLabel_.setText(QString{tr(DEFAULT)}.arg("image"));
+    init();
 }
 
 void
 PreviewUploadOverlay::setPreview(const QByteArray data, const QString &mime)
 {
-        auto const &split = mime.split('/');
-        auto const &type  = split[1];
+    nhlog::ui()->info("Pasting {} bytes of data, mimetype {}", data.size(), mime.toStdString());
+
+    auto const &split = mime.split('/');
+    auto const &type  = split[1];
 
-        data_      = data;
-        mediaType_ = mime;
-        filePath_  = "clipboard." + type;
-        isImage_   = false;
+    data_      = data;
+    mediaType_ = mime;
+    filePath_  = "clipboard." + type;
+    isImage_   = false;
 
-        setLabels(type, mime, data_.size());
-        init();
+    if (mime == "image/svg+xml") {
+        isImage_ = true;
+        image_.loadFromData(data_, mediaType_.toStdString().c_str());
+    }
+
+    setLabels(type, mime, data_.size());
+    init();
 }
 
 void
 PreviewUploadOverlay::setPreview(const QString &path)
 {
-        QFile file{path};
-
-        if (!file.open(QIODevice::ReadOnly)) {
-                nhlog::ui()->warn("Failed to open file ({}): {}",
-                                  path.toStdString(),
-                                  file.errorString().toStdString());
-                close();
-                return;
-        }
+    QFile file{path};
 
-        QMimeDatabase db;
-        auto mime = db.mimeTypeForFileNameAndData(path, &file);
+    if (!file.open(QIODevice::ReadOnly)) {
+        nhlog::ui()->warn(
+          "Failed to open file ({}): {}", path.toStdString(), file.errorString().toStdString());
+        close();
+        return;
+    }
 
-        if ((data_ = file.readAll()).isEmpty()) {
-                nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString());
-                close();
-                return;
-        }
+    QMimeDatabase db;
+    auto mime = db.mimeTypeForFileNameAndData(path, &file);
 
-        auto const &split = mime.name().split('/');
+    if ((data_ = file.readAll()).isEmpty()) {
+        nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString());
+        close();
+        return;
+    }
 
-        mediaType_ = mime.name();
-        filePath_  = file.fileName();
-        isImage_   = false;
+    auto const &split = mime.name().split('/');
 
-        setLabels(split[1], mime.name(), data_.size());
-        init();
+    mediaType_ = mime.name();
+    filePath_  = file.fileName();
+    isImage_   = false;
+
+    setLabels(split[1], mime.name(), data_.size());
+    init();
 }
 
 void
 PreviewUploadOverlay::keyPressEvent(QKeyEvent *event)
 {
-        if (event->matches(QKeySequence::Cancel)) {
-                emit aborted();
-                close();
-        } else {
-                QWidget::keyPressEvent(event);
-        }
+    if (event->matches(QKeySequence::Cancel)) {
+        emit aborted();
+        close();
+    } else {
+        QWidget::keyPressEvent(event);
+    }
 }
\ No newline at end of file
diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h
index d23ea0ae2179bb633586df7a2f2c77d377d9f852..e9078069ada92192ccf5e779461e332535b3de5f 100644
--- a/src/dialogs/PreviewUploadOverlay.h
+++ b/src/dialogs/PreviewUploadOverlay.h
@@ -18,35 +18,35 @@ namespace dialogs {
 
 class PreviewUploadOverlay : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        PreviewUploadOverlay(QWidget *parent = nullptr);
+    PreviewUploadOverlay(QWidget *parent = nullptr);
 
-        void setPreview(const QImage &src, const QString &mime);
-        void setPreview(const QByteArray data, const QString &mime);
-        void setPreview(const QString &path);
-        void keyPressEvent(QKeyEvent *event);
+    void setPreview(const QImage &src, const QString &mime);
+    void setPreview(const QByteArray data, const QString &mime);
+    void setPreview(const QString &path);
+    void keyPressEvent(QKeyEvent *event);
 
 signals:
-        void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
-        void aborted();
+    void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
+    void aborted();
 
 private:
-        void init();
-        void setLabels(const QString &type, const QString &mime, uint64_t upload_size);
+    void init();
+    void setLabels(const QString &type, const QString &mime, uint64_t upload_size);
 
-        bool isImage_;
-        QPixmap image_;
+    bool isImage_;
+    QPixmap image_;
 
-        QByteArray data_;
-        QString filePath_;
-        QString mediaType_;
+    QByteArray data_;
+    QString filePath_;
+    QString mediaType_;
 
-        QLabel titleLabel_;
-        QLabel infoLabel_;
-        QLineEdit fileName_;
+    QLabel titleLabel_;
+    QLabel infoLabel_;
+    QLineEdit fileName_;
 
-        QPushButton upload_;
-        QPushButton cancel_;
+    QPushButton upload_;
+    QPushButton cancel_;
 };
 } // dialogs
diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp
index c7b95f1a836d0a7778a227a88e6fd04e0398a03c..0ae46bbaf610d395d09bbbadc4deb17d8c2c2c8d 100644
--- a/src/dialogs/ReCaptcha.cpp
+++ b/src/dialogs/ReCaptcha.cpp
@@ -18,54 +18,54 @@ using namespace dialogs;
 ReCaptcha::ReCaptcha(const QString &session, QWidget *parent)
   : QWidget(parent)
 {
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
+    setAutoFillBackground(true);
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+    setWindowModality(Qt::WindowModal);
+    setAttribute(Qt::WA_DeleteOnClose, true);
 
-        auto layout = new QVBoxLayout(this);
-        layout->setSpacing(conf::modals::WIDGET_SPACING);
-        layout->setMargin(conf::modals::WIDGET_MARGIN);
+    auto layout = new QVBoxLayout(this);
+    layout->setSpacing(conf::modals::WIDGET_SPACING);
+    layout->setMargin(conf::modals::WIDGET_MARGIN);
 
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(8);
-        buttonLayout->setMargin(0);
+    auto buttonLayout = new QHBoxLayout();
+    buttonLayout->setSpacing(8);
+    buttonLayout->setMargin(0);
 
-        openCaptchaBtn_ = new QPushButton("Open reCAPTCHA", this);
-        cancelBtn_      = new QPushButton(tr("Cancel"), this);
-        confirmBtn_     = new QPushButton(tr("Confirm"), this);
-        confirmBtn_->setDefault(true);
+    openCaptchaBtn_ = new QPushButton("Open reCAPTCHA", this);
+    cancelBtn_      = new QPushButton(tr("Cancel"), this);
+    confirmBtn_     = new QPushButton(tr("Confirm"), this);
+    confirmBtn_->setDefault(true);
 
-        buttonLayout->addStretch(1);
-        buttonLayout->addWidget(openCaptchaBtn_);
-        buttonLayout->addWidget(cancelBtn_);
-        buttonLayout->addWidget(confirmBtn_);
+    buttonLayout->addStretch(1);
+    buttonLayout->addWidget(openCaptchaBtn_);
+    buttonLayout->addWidget(cancelBtn_);
+    buttonLayout->addWidget(confirmBtn_);
 
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
+    QFont font;
+    font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
 
-        auto label = new QLabel(tr("Solve the reCAPTCHA and press the confirm button"), this);
-        label->setFont(font);
+    auto label = new QLabel(tr("Solve the reCAPTCHA and press the confirm button"), this);
+    label->setFont(font);
 
-        layout->addWidget(label);
-        layout->addLayout(buttonLayout);
+    layout->addWidget(label);
+    layout->addLayout(buttonLayout);
 
-        connect(openCaptchaBtn_, &QPushButton::clicked, [session]() {
-                const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/"
-                                         "fallback/web?session=%3")
-                                   .arg(QString::fromStdString(http::client()->server()))
-                                   .arg(http::client()->port())
-                                   .arg(session);
+    connect(openCaptchaBtn_, &QPushButton::clicked, [session]() {
+        const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/"
+                                 "fallback/web?session=%3")
+                           .arg(QString::fromStdString(http::client()->server()))
+                           .arg(http::client()->port())
+                           .arg(session);
 
-                QDesktopServices::openUrl(url);
-        });
+        QDesktopServices::openUrl(url);
+    });
 
-        connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
-                emit confirmation();
-                emit close();
-        });
-        connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
-                emit cancel();
-                emit close();
-        });
+    connect(confirmBtn_, &QPushButton::clicked, this, [this]() {
+        emit confirmation();
+        emit close();
+    });
+    connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
+        emit cancel();
+        emit close();
+    });
 }
diff --git a/src/dialogs/ReCaptcha.h b/src/dialogs/ReCaptcha.h
index 0c9f75390448924d51e56213564b596202029dbc..1e69de66acbe4e16a5c2b9b363d9f3bd719b5dcb 100644
--- a/src/dialogs/ReCaptcha.h
+++ b/src/dialogs/ReCaptcha.h
@@ -12,18 +12,18 @@ namespace dialogs {
 
 class ReCaptcha : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        ReCaptcha(const QString &session, QWidget *parent = nullptr);
+    ReCaptcha(const QString &session, QWidget *parent = nullptr);
 
 signals:
-        void confirmation();
-        void cancel();
+    void confirmation();
+    void cancel();
 
 private:
-        QPushButton *openCaptchaBtn_;
-        QPushButton *confirmBtn_;
-        QPushButton *cancelBtn_;
+    QPushButton *openCaptchaBtn_;
+    QPushButton *confirmBtn_;
+    QPushButton *cancelBtn_;
 };
 } // dialogs
diff --git a/src/emoji/EmojiModel.cpp b/src/emoji/EmojiModel.cpp
index 66e7aeda99bbd89ec6f2c26fe1e96841095308d3..07e6fdbde9fce157a37312fe4f72948c9cd2ef67 100644
--- a/src/emoji/EmojiModel.cpp
+++ b/src/emoji/EmojiModel.cpp
@@ -14,63 +14,60 @@ using namespace emoji;
 int
 EmojiModel::categoryToIndex(int category)
 {
-        auto dist = std::distance(Provider::emoji.begin(),
-                                  std::lower_bound(Provider::emoji.begin(),
-                                                   Provider::emoji.end(),
-                                                   static_cast<Emoji::Category>(category),
-                                                   [](const struct Emoji &e, Emoji::Category c) {
-                                                           return e.category < c;
-                                                   }));
+    auto dist = std::distance(
+      Provider::emoji.begin(),
+      std::lower_bound(Provider::emoji.begin(),
+                       Provider::emoji.end(),
+                       static_cast<Emoji::Category>(category),
+                       [](const struct Emoji &e, Emoji::Category c) { return e.category < c; }));
 
-        return static_cast<int>(dist);
+    return static_cast<int>(dist);
 }
 
 QHash<int, QByteArray>
 EmojiModel::roleNames() const
 {
-        static QHash<int, QByteArray> roles;
+    static QHash<int, QByteArray> roles;
 
-        if (roles.isEmpty()) {
-                roles = QAbstractListModel::roleNames();
-                roles[static_cast<int>(EmojiModel::Roles::Unicode)] = QByteArrayLiteral("unicode");
-                roles[static_cast<int>(EmojiModel::Roles::ShortName)] =
-                  QByteArrayLiteral("shortName");
-                roles[static_cast<int>(EmojiModel::Roles::Category)] =
-                  QByteArrayLiteral("category");
-                roles[static_cast<int>(EmojiModel::Roles::Emoji)] = QByteArrayLiteral("emoji");
-        }
+    if (roles.isEmpty()) {
+        roles                                                 = QAbstractListModel::roleNames();
+        roles[static_cast<int>(EmojiModel::Roles::Unicode)]   = QByteArrayLiteral("unicode");
+        roles[static_cast<int>(EmojiModel::Roles::ShortName)] = QByteArrayLiteral("shortName");
+        roles[static_cast<int>(EmojiModel::Roles::Category)]  = QByteArrayLiteral("category");
+        roles[static_cast<int>(EmojiModel::Roles::Emoji)]     = QByteArrayLiteral("emoji");
+    }
 
-        return roles;
+    return roles;
 }
 
 int
 EmojiModel::rowCount(const QModelIndex &parent) const
 {
-        return parent == QModelIndex() ? Provider::emoji.count() : 0;
+    return parent == QModelIndex() ? Provider::emoji.count() : 0;
 }
 
 QVariant
 EmojiModel::data(const QModelIndex &index, int role) const
 {
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                switch (role) {
-                case Qt::DisplayRole:
-                case CompletionModel::CompletionRole:
-                case static_cast<int>(EmojiModel::Roles::Unicode):
-                        return Provider::emoji[index.row()].unicode;
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        switch (role) {
+        case Qt::DisplayRole:
+        case CompletionModel::CompletionRole:
+        case static_cast<int>(EmojiModel::Roles::Unicode):
+            return Provider::emoji[index.row()].unicode;
 
-                case Qt::ToolTipRole:
-                case CompletionModel::SearchRole:
-                case static_cast<int>(EmojiModel::Roles::ShortName):
-                        return Provider::emoji[index.row()].shortName;
+        case Qt::ToolTipRole:
+        case CompletionModel::SearchRole:
+        case static_cast<int>(EmojiModel::Roles::ShortName):
+            return Provider::emoji[index.row()].shortName;
 
-                case static_cast<int>(EmojiModel::Roles::Category):
-                        return QVariant::fromValue(Provider::emoji[index.row()].category);
+        case static_cast<int>(EmojiModel::Roles::Category):
+            return QVariant::fromValue(Provider::emoji[index.row()].category);
 
-                case static_cast<int>(EmojiModel::Roles::Emoji):
-                        return QVariant::fromValue(Provider::emoji[index.row()]);
-                }
+        case static_cast<int>(EmojiModel::Roles::Emoji):
+            return QVariant::fromValue(Provider::emoji[index.row()]);
         }
+    }
 
-        return {};
+    return {};
 }
diff --git a/src/emoji/EmojiModel.h b/src/emoji/EmojiModel.h
index 679563f1fbcca535408246fa343e9ead1421c9a3..882d3eb8933b573e8ddff89a07085c25f309e0b0 100644
--- a/src/emoji/EmojiModel.h
+++ b/src/emoji/EmojiModel.h
@@ -18,22 +18,22 @@ namespace emoji {
  */
 class EmojiModel : public QAbstractListModel
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        enum Roles
-        {
-                Unicode = Qt::UserRole, // unicode of emoji
-                Category,               // category of emoji
-                ShortName,              // shortext of the emoji
-                Emoji,                  // Contains everything from the Emoji
-        };
+    enum Roles
+    {
+        Unicode = Qt::UserRole, // unicode of emoji
+        Category,               // category of emoji
+        ShortName,              // shortext of the emoji
+        Emoji,                  // Contains everything from the Emoji
+    };
 
-        using QAbstractListModel::QAbstractListModel;
+    using QAbstractListModel::QAbstractListModel;
 
-        Q_INVOKABLE int categoryToIndex(int category);
+    Q_INVOKABLE int categoryToIndex(int category);
 
-        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;
+    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;
 };
 }
diff --git a/src/emoji/MacHelper.h b/src/emoji/MacHelper.h
index b3e2e6314f541892e7d8352441fb31f2c469c160..ff49f9ba6ac063c0e18fe8a99fc4778b284a7db5 100644
--- a/src/emoji/MacHelper.h
+++ b/src/emoji/MacHelper.h
@@ -9,6 +9,6 @@
 class MacHelper
 {
 public:
-        static void showEmojiWindow();
-        static void initializeMenus();
+    static void showEmojiWindow();
+    static void initializeMenus();
 };
diff --git a/src/emoji/Provider.cpp b/src/emoji/Provider.cpp
index 70ac474eeccefc2b7d4ff62a4024bd7453d9db0c..d62eeee4507c213c2f8f73122243f7276c67c08b 100644
--- a/src/emoji/Provider.cpp
+++ b/src/emoji/Provider.cpp
@@ -34,6 +34,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "slightly smiling face",
         emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x99\x83"), "upside-down face", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa0"), "melting face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\x89"), "winking face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\x8a"),
         "smiling face with smiling eyes",
@@ -74,12 +75,21 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "squinting face with tongue",
         emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x91"), "money-mouth face", emoji::Emoji::Category::People},
-  Emoji{QString::fromUtf8("\xf0\x9f\xa4\x97"), "hugging face", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa4\x97"),
+        "smiling face with open hands",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xad"),
         "face with hand over mouth",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa2"),
+        "face with open eyes and hand over mouth",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa3"),
+        "face with peeking eye",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xab"), "shushing face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x94"), "thinking face", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa1"), "saluting face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x90"), "zipper-mouth face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa8"),
         "face with raised eyebrow",
@@ -91,6 +101,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x98\xb6"),
         "face without mouth",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa5"), "dotted line face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\xb6\xe2\x80\x8d\xf0\x9f\x8c\xab"),
         "face in clouds",
         emoji::Emoji::Category::People},
@@ -124,7 +135,9 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb5"), "hot face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb6"), "cold face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb4"), "woozy face", emoji::Emoji::Category::People},
-  Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5"), "knocked-out face", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5"),
+        "face with crossed-out eyes",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5\xe2\x80\x8d\xf0\x9f\x92\xab"),
         "face with spiral eyes",
         emoji::Emoji::Category::People},
@@ -138,6 +151,9 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x93"), "nerd face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\x90"), "face with monocle", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\x95"), "confused face", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa4"),
+        "face with diagonal mouth",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\x9f"), "worried face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x99\x81"),
         "slightly frowning face",
@@ -150,6 +166,9 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x98\xb2"), "astonished face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\xb3"), "flushed face", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\xba"), "pleading face", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb9"),
+        "face holding back tears",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x98\xa6"),
         "frowning face with open mouth",
         emoji::Emoji::Category::People},
@@ -367,6 +386,70 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbf"),
         "vulcan salute: dark skin tone",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb1"), "rightwards hand", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb"),
+        "rightwards hand: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc"),
+        "rightwards hand: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd"),
+        "rightwards hand: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe"),
+        "rightwards hand: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf"),
+        "rightwards hand: dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb2"), "leftwards hand", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb"),
+        "leftwards hand: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc"),
+        "leftwards hand: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd"),
+        "leftwards hand: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe"),
+        "leftwards hand: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf"),
+        "leftwards hand: dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb3"), "palm down hand", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbb"),
+        "palm down hand: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbc"),
+        "palm down hand: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbd"),
+        "palm down hand: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbe"),
+        "palm down hand: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbf"),
+        "palm down hand: dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb4"), "palm up hand", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbb"),
+        "palm up hand: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbc"),
+        "palm up hand: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbd"),
+        "palm up hand: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbe"),
+        "palm up hand: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbf"),
+        "palm up hand: dark skin tone",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c"), "OK hand", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbb"),
         "OK hand: light skin tone",
@@ -447,6 +530,24 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbf"),
         "crossed fingers: dark skin tone",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb0"),
+        "hand with index finger and thumb crossed",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbb"),
+        "hand with index finger and thumb crossed: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbc"),
+        "hand with index finger and thumb crossed: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbd"),
+        "hand with index finger and thumb crossed: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbe"),
+        "hand with index finger and thumb crossed: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbf"),
+        "hand with index finger and thumb crossed: dark skin tone",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f"), "love-you gesture", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbb"),
         "love-you gesture: light skin tone",
@@ -599,6 +700,24 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbf"),
         "index pointing up: dark skin tone",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb5"),
+        "index pointing at the viewer",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbb"),
+        "index pointing at the viewer: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbc"),
+        "index pointing at the viewer: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbd"),
+        "index pointing at the viewer: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbe"),
+        "index pointing at the viewer: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbf"),
+        "index pointing at the viewer: dark skin tone",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d"), "thumbs up", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbb"),
         "thumbs up: light skin tone",
@@ -727,6 +846,22 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbf"),
         "raising hands: dark skin tone",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb6"), "heart hands", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbb"),
+        "heart hands: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbc"),
+        "heart hands: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbd"),
+        "heart hands: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbe"),
+        "heart hands: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbf"),
+        "heart hands: dark skin tone",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x90"), "open hands", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbb"),
         "open hands: light skin tone",
@@ -760,6 +895,101 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "palms up together: dark skin tone",
         emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d"), "handshake", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbb"),
+        "handshake: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbc"),
+        "handshake: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbd"),
+        "handshake: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbe"),
+        "handshake: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbf"),
+        "handshake: dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc"),
+        "handshake: light skin tone, medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd"),
+        "handshake: light skin tone, medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe"),
+        "handshake: light skin tone, medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf"),
+        "handshake: light skin tone, dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb"),
+        "handshake: medium-light skin tone, light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd"),
+        "handshake: medium-light skin tone, medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe"),
+        "handshake: medium-light skin tone, medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf"),
+        "handshake: medium-light skin tone, dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb"),
+        "handshake: medium skin tone, light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc"),
+        "handshake: medium skin tone, medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe"),
+        "handshake: medium skin tone, medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf"),
+        "handshake: medium skin tone, dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb"),
+        "handshake: medium-dark skin tone, light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc"),
+        "handshake: medium-dark skin tone, medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd"),
+        "handshake: medium-dark skin tone, medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf"),
+        "handshake: medium-dark skin tone, dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb"),
+        "handshake: dark skin tone, light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc"),
+        "handshake: dark skin tone, medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd"),
+        "handshake: dark skin tone, medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8(
+          "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe"),
+        "handshake: dark skin tone, medium-dark skin tone",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f"), "folded hands", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbb"),
         "folded hands: light skin tone",
@@ -933,6 +1163,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x81"), "eye", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x85"), "tongue", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x84"), "mouth", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa6"), "biting lip", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6"), "baby", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbb"),
         "baby: light skin tone",
@@ -3041,6 +3272,22 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"),
         "woman construction worker: dark skin tone",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x85"), "person with crown", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x85\xf0\x9f\x8f\xbb"),
+        "person with crown: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x85\xf0\x9f\x8f\xbc"),
+        "person with crown: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x85\xf0\x9f\x8f\xbd"),
+        "person with crown: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x85\xf0\x9f\x8f\xbe"),
+        "person with crown: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x85\xf0\x9f\x8f\xbf"),
+        "person with crown: dark skin tone",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4"), "prince", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbb"),
         "prince: light skin tone",
@@ -3283,6 +3530,38 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbf"),
         "pregnant woman: dark skin tone",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x83"), "pregnant man", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x83\xf0\x9f\x8f\xbb"),
+        "pregnant man: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x83\xf0\x9f\x8f\xbc"),
+        "pregnant man: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x83\xf0\x9f\x8f\xbd"),
+        "pregnant man: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x83\xf0\x9f\x8f\xbe"),
+        "pregnant man: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x83\xf0\x9f\x8f\xbf"),
+        "pregnant man: dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x84"), "pregnant person", emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x84\xf0\x9f\x8f\xbb"),
+        "pregnant person: light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x84\xf0\x9f\x8f\xbc"),
+        "pregnant person: medium-light skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x84\xf0\x9f\x8f\xbd"),
+        "pregnant person: medium skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x84\xf0\x9f\x8f\xbe"),
+        "pregnant person: medium-dark skin tone",
+        emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x84\xf0\x9f\x8f\xbf"),
+        "pregnant person: dark skin tone",
+        emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1"), "breast-feeding", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbb"),
         "breast-feeding: light skin tone",
@@ -3797,6 +4076,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9f\xe2\x80\x8d\xe2\x99\x80"),
         "woman zombie",
         emoji::Emoji::Category::People},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8c"), "troll", emoji::Emoji::Category::People},
   Emoji{QString::fromUtf8("\xf0\x9f\x92\x86"),
         "person getting massage",
         emoji::Emoji::Category::People},
@@ -7432,6 +7712,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa6\x88"), "shark", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x90\x99"), "octopus", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x90\x9a"), "spiral shell", emoji::Emoji::Category::Nature},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb8"), "coral", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x90\x8c"), "snail", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8b"), "butterfly", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x90\x9b"), "bug", emoji::Emoji::Category::Nature},
@@ -7451,6 +7732,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x92\x90"), "bouquet", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb8"), "cherry blossom", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x92\xae"), "white flower", emoji::Emoji::Category::Nature},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb7"), "lotus", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb5"), "rosette", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb9"), "rose", emoji::Emoji::Category::Nature},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\x80"), "wilted flower", emoji::Emoji::Category::Nature},
@@ -7473,6 +7755,8 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x8d\x83"),
         "leaf fluttering in wind",
         emoji::Emoji::Category::Nature},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb9"), "empty nest", emoji::Emoji::Category::Nature},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xba"), "nest with eggs", emoji::Emoji::Category::Nature},
   // Food
   Emoji{QString::fromUtf8("\xf0\x9f\x8d\x87"), "grapes", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\x8d\x88"), "melon", emoji::Emoji::Category::Food},
@@ -7507,6 +7791,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\x85"), "onion", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\x8d\x84"), "mushroom", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9c"), "peanuts", emoji::Emoji::Category::Food},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x98"), "beans", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb0"), "chestnut", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9e"), "bread", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\x90"), "croissant", emoji::Emoji::Category::Food},
@@ -7600,6 +7885,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbb"), "clinking beer mugs", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\x82"), "clinking glasses", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\x83"), "tumbler glass", emoji::Emoji::Category::Food},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x97"), "pouring liquid", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa4"), "cup with straw", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8b"), "bubble tea", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\x83"), "beverage box", emoji::Emoji::Category::Food},
@@ -7612,6 +7898,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb4"), "fork and knife", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\x84"), "spoon", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\x94\xaa"), "kitchen knife", emoji::Emoji::Category::Food},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\x99"), "jar", emoji::Emoji::Category::Food},
   Emoji{QString::fromUtf8("\xf0\x9f\x8f\xba"), "amphora", emoji::Emoji::Category::Food},
   // Activity
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\x83"), "jack-o-lantern", emoji::Emoji::Category::Activity},
@@ -7683,13 +7970,15 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x94\xae"), "crystal ball", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\xaa\x84"), "magic wand", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbf"), "nazar amulet", emoji::Emoji::Category::Activity},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xac"), "hamsa", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\xae"), "video game", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xb9"), "joystick", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb0"), "slot machine", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb2"), "game die", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa9"), "puzzle piece", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb8"), "teddy bear", emoji::Emoji::Category::Activity},
-  Emoji{QString::fromUtf8("\xf0\x9f\xaa\x85"), "piñata", emoji::Emoji::Category::Activity},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\x85"), "pi�ata", emoji::Emoji::Category::Activity},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa9"), "mirror ball", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xf0\x9f\xaa\x86"), "nesting dolls", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xe2\x99\xa0"), "spade suit", emoji::Emoji::Category::Activity},
   Emoji{QString::fromUtf8("\xe2\x99\xa5"), "heart suit", emoji::Emoji::Category::Activity},
@@ -7792,6 +8081,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x8c\x89"), "bridge at night", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xe2\x99\xa8"), "hot springs", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa0"), "carousel horse", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x9b\x9d"), "playground slide", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa1"), "ferris wheel", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa2"), "roller coaster", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x92\x88"), "barber pole", emoji::Emoji::Category::Travel},
@@ -7848,6 +8138,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa4"), "railway track", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa2"), "oil drum", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xe2\x9b\xbd"), "fuel pump", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x9b\x9e"), "wheel", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa8"), "police car light", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa5"),
         "horizontal traffic light",
@@ -7858,6 +8149,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x9b\x91"), "stop sign", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa7"), "construction", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xe2\x9a\x93"), "anchor", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x9b\x9f"), "ring buoy", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xe2\x9b\xb5"), "sailboat", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb6"), "canoe", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa4"), "speedboat", emoji::Emoji::Category::Travel},
@@ -7891,29 +8183,29 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xe2\x8f\xb1"), "stopwatch", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xe2\x8f\xb2"), "timer clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xb0"), "mantelpiece clock", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x9b"), "twelve o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x9b"), "twelve o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa7"), "twelve-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x90"), "one o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x90"), "one o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\x9c"), "one-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x91"), "two o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x91"), "two o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\x9d"), "two-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x92"), "three o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x92"), "three o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\x9e"), "three-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x93"), "four o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x93"), "four o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\x9f"), "four-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x94"), "five o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x94"), "five o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa0"), "five-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x95"), "six o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x95"), "six o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa1"), "six-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x96"), "seven o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x96"), "seven o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa2"), "seven-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x97"), "eight o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x97"), "eight o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa3"), "eight-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x98"), "nine o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x98"), "nine o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa4"), "nine-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x99"), "ten o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x99"), "ten o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa5"), "ten-thirty", emoji::Emoji::Category::Travel},
-  Emoji{QString::fromUtf8("\xf0\x9f\x95\x9a"), "eleven o’clock", emoji::Emoji::Category::Travel},
+  Emoji{QString::fromUtf8("\xf0\x9f\x95\x9a"), "eleven o�clock", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x95\xa6"), "eleven-thirty", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x8c\x91"), "new moon", emoji::Emoji::Category::Travel},
   Emoji{QString::fromUtf8("\xf0\x9f\x8c\x92"),
@@ -8010,29 +8302,29 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb2"), "briefs", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb3"), "shorts", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x99"), "bikini", emoji::Emoji::Category::Objects},
-  Emoji{QString::fromUtf8("\xf0\x9f\x91\x9a"), "woman’s clothes", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\x91\x9a"), "woman�s clothes", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x9b"), "purse", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x9c"), "handbag", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x9d"), "clutch bag", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8d"), "shopping bags", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\x92"), "backpack", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb4"), "thong sandal", emoji::Emoji::Category::Objects},
-  Emoji{QString::fromUtf8("\xf0\x9f\x91\x9e"), "man’s shoe", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\x91\x9e"), "man�s shoe", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x9f"), "running shoe", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbe"), "hiking boot", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbf"), "flat shoe", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\xa0"), "high-heeled shoe", emoji::Emoji::Category::Objects},
-  Emoji{QString::fromUtf8("\xf0\x9f\x91\xa1"), "woman’s sandal", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\x91\xa1"), "woman�s sandal", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb0"), "ballet shoes", emoji::Emoji::Category::Objects},
-  Emoji{QString::fromUtf8("\xf0\x9f\x91\xa2"), "woman’s boot", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\x91\xa2"), "woman�s boot", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x91\x91"), "crown", emoji::Emoji::Category::Objects},
-  Emoji{QString::fromUtf8("\xf0\x9f\x91\x92"), "woman’s hat", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\x91\x92"), "woman�s hat", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa9"), "top hat", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x8e\x93"), "graduation cap", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa2"), "billed cap", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xaa\x96"), "military helmet", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xe2\x9b\x91"),
-        "rescue worker’s helmet",
+        "rescue worker�s helmet",
         emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x93\xbf"), "prayer beads", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x92\x84"), "lipstick", emoji::Emoji::Category::Objects},
@@ -8084,6 +8376,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x93\x9f"), "pager", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x93\xa0"), "fax machine", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x94\x8b"), "battery", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xab"), "low battery", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x94\x8c"), "electric plug", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x92\xbb"), "laptop", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x96\xa5"), "desktop computer", emoji::Emoji::Category::Objects},
@@ -8262,7 +8555,9 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb8"), "drop of blood", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x92\x8a"), "pill", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb9"), "adhesive bandage", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa9\xbc"), "crutch", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa9\xba"), "stethoscope", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\xa9\xbb"), "x-ray", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaa"), "door", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x9b\x97"), "elevator", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9e"), "mirror", emoji::Emoji::Category::Objects},
@@ -8283,6 +8578,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbb"), "roll of paper", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa3"), "bucket", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbc"), "soap", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\xab\xa7"), "bubbles", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa5"), "toothbrush", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbd"), "sponge", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xa7\xaf"),
@@ -8295,6 +8591,9 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xe2\x9a\xb1"), "funeral urn", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\x97\xbf"), "moai", emoji::Emoji::Category::Objects},
   Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa7"), "placard", emoji::Emoji::Category::Objects},
+  Emoji{QString::fromUtf8("\xf0\x9f\xaa\xaa"),
+        "identification card",
+        emoji::Emoji::Category::Objects},
   // Symbols
   Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa7"), "ATM sign", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xae"),
@@ -8302,8 +8601,8 @@ const QVector<Emoji> emoji::Provider::emoji = {
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb0"), "potable water", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xe2\x99\xbf"), "wheelchair symbol", emoji::Emoji::Category::Symbols},
-  Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb9"), "men’s room", emoji::Emoji::Category::Symbols},
-  Emoji{QString::fromUtf8("\xf0\x9f\x9a\xba"), "women’s room", emoji::Emoji::Category::Symbols},
+  Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb9"), "men�s room", emoji::Emoji::Category::Symbols},
+  Emoji{QString::fromUtf8("\xf0\x9f\x9a\xba"), "women�s room", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbb"), "restroom", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbc"), "baby symbol", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbe"), "water closet", emoji::Emoji::Category::Symbols},
@@ -8425,6 +8724,9 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xe2\x9e\x95"), "plus", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xe2\x9e\x96"), "minus", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xe2\x9e\x97"), "divide", emoji::Emoji::Category::Symbols},
+  Emoji{QString::fromUtf8("\xf0\x9f\x9f\xb0"),
+        "heavy equals sign",
+        emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xe2\x99\xbe"), "infinity", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xe2\x80\xbc"),
         "double exclamation mark",
@@ -8558,55 +8860,55 @@ const QVector<Emoji> emoji::Provider::emoji = {
   Emoji{QString::fromUtf8("\xf0\x9f\x86\x99"), "UP! button", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x86\x9a"), "VS button", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\x81"),
-        "Japanese “here” button",
+        "Japanese �here� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\x82"),
-        "Japanese “service charge” button",
+        "Japanese �service charge� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb7"),
-        "Japanese “monthly amount” button",
+        "Japanese �monthly amount� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb6"),
-        "Japanese “not free of charge” button",
+        "Japanese �not free of charge� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xaf"),
-        "Japanese “reserved” button",
+        "Japanese �reserved� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x89\x90"),
-        "Japanese “bargain” button",
+        "Japanese �bargain� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb9"),
-        "Japanese “discount” button",
+        "Japanese �discount� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\x9a"),
-        "Japanese “free of charge” button",
+        "Japanese �free of charge� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb2"),
-        "Japanese “prohibited” button",
+        "Japanese �prohibited� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x89\x91"),
-        "Japanese “acceptable” button",
+        "Japanese �acceptable� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb8"),
-        "Japanese “application” button",
+        "Japanese �application� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb4"),
-        "Japanese “passing grade” button",
+        "Japanese �passing grade� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb3"),
-        "Japanese “vacancy” button",
+        "Japanese �vacancy� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xe3\x8a\x97"),
-        "Japanese “congratulations” button",
+        "Japanese �congratulations� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xe3\x8a\x99"),
-        "Japanese “secret” button",
+        "Japanese �secret� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xba"),
-        "Japanese “open for business” button",
+        "Japanese �open for business� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x88\xb5"),
-        "Japanese “no vacancy” button",
+        "Japanese �no vacancy� button",
         emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x94\xb4"), "red circle", emoji::Emoji::Category::Symbols},
   Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa0"), "orange circle", emoji::Emoji::Category::Symbols},
@@ -8731,7 +9033,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "flag: Aruba",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbd"),
-        "flag: Ã…land Islands",
+        "flag: �land Islands",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbf"),
         "flag: Azerbaijan",
@@ -8764,7 +9066,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "flag: Benin",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb1"),
-        "flag: St. Barthélemy",
+        "flag: St. Barth�lemy",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb2"),
         "flag: Bermuda",
@@ -8818,7 +9120,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "flag: Switzerland",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xae"),
-        "flag: Côte d’Ivoire",
+        "flag: C�te d�Ivoire",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb0"),
         "flag: Cook Islands",
@@ -8848,7 +9150,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "flag: Cape Verde",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbc"),
-        "flag: Curaçao",
+        "flag: Cura�ao",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbd"),
         "flag: Christmas Island",
@@ -9265,7 +9567,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "flag: Qatar",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xaa"),
-        "flag: Réunion",
+        "flag: R�union",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb4"),
         "flag: Romania",
@@ -9328,7 +9630,7 @@ const QVector<Emoji> emoji::Provider::emoji = {
         "flag: South Sudan",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb9"),
-        "flag: São Tomé & Príncipe",
+        "flag: S�o Tom� & Pr�ncipe",
         emoji::Emoji::Category::Flags},
   Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbb"),
         "flag: El Salvador",
@@ -9471,4 +9773,4 @@ const QVector<Emoji> emoji::Provider::emoji = {
                           "\x81\xac\xf3\xa0\x81\xb3\xf3\xa0\x81\xbf"),
         "flag: Wales",
         emoji::Emoji::Category::Flags},
-};
+};
\ No newline at end of file
diff --git a/src/emoji/Provider.h b/src/emoji/Provider.h
index 43c880a208f49b38d878ac70ee219eb6447e1faf..965329f9ff4749b82c8d60467984a63d46755684 100644
--- a/src/emoji/Provider.h
+++ b/src/emoji/Provider.h
@@ -16,37 +16,37 @@ Q_NAMESPACE
 
 struct Emoji
 {
-        Q_GADGET
+    Q_GADGET
 public:
-        enum class Category
-        {
-                People,
-                Nature,
-                Food,
-                Activity,
-                Travel,
-                Objects,
-                Symbols,
-                Flags,
-                Search
-        };
-        Q_ENUM(Category)
-
-        Q_PROPERTY(const QString &unicode MEMBER unicode)
-        Q_PROPERTY(const QString &shortName MEMBER shortName)
-        Q_PROPERTY(emoji::Emoji::Category category MEMBER category)
+    enum class Category
+    {
+        People,
+        Nature,
+        Food,
+        Activity,
+        Travel,
+        Objects,
+        Symbols,
+        Flags,
+        Search
+    };
+    Q_ENUM(Category)
+
+    Q_PROPERTY(const QString &unicode MEMBER unicode)
+    Q_PROPERTY(const QString &shortName MEMBER shortName)
+    Q_PROPERTY(emoji::Emoji::Category category MEMBER category)
 
 public:
-        QString unicode;
-        QString shortName;
-        Category category;
+    QString unicode;
+    QString shortName;
+    Category category;
 };
 
 class Provider
 {
 public:
-        // all emoji for QML purposes
-        static const QVector<Emoji> emoji;
+    // all emoji for QML purposes
+    static const QVector<Emoji> emoji;
 };
 
 } // namespace emoji
diff --git a/src/encryption/DeviceVerificationFlow.cpp b/src/encryption/DeviceVerificationFlow.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f05d5c9ff8147ae4487bf5ebcf83b6ba6e1ce881
--- /dev/null
+++ b/src/encryption/DeviceVerificationFlow.cpp
@@ -0,0 +1,885 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "DeviceVerificationFlow.h"
+
+#include "Cache.h"
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "Utils.h"
+#include "timeline/TimelineModel.h"
+
+#include <QDateTime>
+#include <QTimer>
+#include <iostream>
+
+static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
+
+namespace msgs = mtx::events::msg;
+
+static mtx::events::msg::KeyVerificationMac
+key_verification_mac(mtx::crypto::SAS *sas,
+                     mtx::identifiers::User sender,
+                     const std::string &senderDevice,
+                     mtx::identifiers::User receiver,
+                     const std::string &receiverDevice,
+                     const std::string &transactionId,
+                     std::map<std::string, std::string> keys);
+
+DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
+                                               DeviceVerificationFlow::Type flow_type,
+                                               TimelineModel *model,
+                                               QString userID,
+                                               std::vector<QString> deviceIds_)
+  : sender(false)
+  , type(flow_type)
+  , deviceIds(std::move(deviceIds_))
+  , model_(model)
+{
+    if (deviceIds.size() == 1)
+        deviceId = deviceIds.front();
+
+    timeout = new QTimer(this);
+    timeout->setSingleShot(true);
+    this->sas           = olm::client()->sas_init();
+    this->isMacVerified = false;
+
+    auto user_id   = userID.toStdString();
+    this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id);
+    cache::client()->query_keys(
+      user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to query device keys: {},{}",
+                                 mtx::errors::to_string(err->matrix_error.errcode),
+                                 static_cast<int>(err->status_code));
+              return;
+          }
+
+          if (!this->deviceId.isEmpty() &&
+              (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
+              nhlog::net()->warn("no devices retrieved {}", user_id);
+              return;
+          }
+
+          this->their_keys = res;
+      });
+
+    cache::client()->query_keys(
+      http::client()->user_id().to_string(),
+      [this](const UserKeyCache &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to query device keys: {},{}",
+                                 mtx::errors::to_string(err->matrix_error.errcode),
+                                 static_cast<int>(err->status_code));
+              return;
+          }
+
+          if (res.master_keys.keys.empty())
+              return;
+
+          if (auto status = cache::verificationStatus(http::client()->user_id().to_string());
+              status && status->user_verified == crypto::Trust::Verified)
+              this->our_trusted_master_key = res.master_keys.keys.begin()->second;
+      });
+
+    if (model) {
+        connect(
+          this->model_, &TimelineModel::updateFlowEventId, this, [this](std::string event_id_) {
+              this->relation.rel_type = mtx::common::RelationType::Reference;
+              this->relation.event_id = event_id_;
+              this->transaction_id    = event_id_;
+          });
+    }
+
+    connect(timeout, &QTimer::timeout, this, [this]() {
+        nhlog::crypto()->info("verification: timeout");
+        if (state_ != Success && state_ != Failed)
+            this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
+    });
+
+    connect(ChatPage::instance(),
+            &ChatPage::receivedDeviceVerificationStart,
+            this,
+            &DeviceVerificationFlow::handleStartMessage);
+    connect(ChatPage::instance(),
+            &ChatPage::receivedDeviceVerificationAccept,
+            this,
+            [this](const mtx::events::msg::KeyVerificationAccept &msg) {
+                nhlog::crypto()->info("verification: received accept");
+                if (msg.transaction_id.has_value()) {
+                    if (msg.transaction_id.value() != this->transaction_id)
+                        return;
+                } else if (msg.relations.references()) {
+                    if (msg.relations.references() != this->relation.event_id)
+                        return;
+                }
+                if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
+                    (msg.hash == "sha256") &&
+                    (msg.message_authentication_code == "hkdf-hmac-sha256")) {
+                    this->commitment = msg.commitment;
+                    if (std::find(msg.short_authentication_string.begin(),
+                                  msg.short_authentication_string.end(),
+                                  mtx::events::msg::SASMethods::Emoji) !=
+                        msg.short_authentication_string.end()) {
+                        this->method = mtx::events::msg::SASMethods::Emoji;
+                    } else {
+                        this->method = mtx::events::msg::SASMethods::Decimal;
+                    }
+                    this->mac_method = msg.message_authentication_code;
+                    this->sendVerificationKey();
+                } else {
+                    this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
+                }
+            });
+
+    connect(ChatPage::instance(),
+            &ChatPage::receivedDeviceVerificationCancel,
+            this,
+            [this](const mtx::events::msg::KeyVerificationCancel &msg) {
+                nhlog::crypto()->info("verification: received cancel");
+                if (msg.transaction_id.has_value()) {
+                    if (msg.transaction_id.value() != this->transaction_id)
+                        return;
+                } else if (msg.relations.references()) {
+                    if (msg.relations.references() != this->relation.event_id)
+                        return;
+                }
+                error_ = User;
+                emit errorChanged();
+                setState(Failed);
+            });
+
+    connect(
+      ChatPage::instance(),
+      &ChatPage::receivedDeviceVerificationKey,
+      this,
+      [this](const mtx::events::msg::KeyVerificationKey &msg) {
+          nhlog::crypto()->info("verification: received key");
+          if (msg.transaction_id.has_value()) {
+              if (msg.transaction_id.value() != this->transaction_id)
+                  return;
+          } else if (msg.relations.references()) {
+              if (msg.relations.references() != this->relation.event_id)
+                  return;
+          }
+
+          if (sender) {
+              if (state_ != WaitingForOtherToAccept) {
+                  this->cancelVerification(OutOfOrder);
+                  return;
+              }
+          } else {
+              if (state_ != WaitingForKeys) {
+                  this->cancelVerification(OutOfOrder);
+                  return;
+              }
+          }
+
+          this->sas->set_their_key(msg.key);
+          std::string info;
+          if (this->sender == true) {
+              info = "MATRIX_KEY_VERIFICATION_SAS|" + http::client()->user_id().to_string() + "|" +
+                     http::client()->device_id() + "|" + this->sas->public_key() + "|" +
+                     this->toClient.to_string() + "|" + this->deviceId.toStdString() + "|" +
+                     msg.key + "|" + this->transaction_id;
+          } else {
+              info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() + "|" +
+                     this->deviceId.toStdString() + "|" + msg.key + "|" +
+                     http::client()->user_id().to_string() + "|" + http::client()->device_id() +
+                     "|" + this->sas->public_key() + "|" + this->transaction_id;
+          }
+
+          nhlog::ui()->info("Info is: '{}'", info);
+
+          if (this->sender == false) {
+              this->sendVerificationKey();
+          } else {
+              if (this->commitment != mtx::crypto::bin2base64_unpadded(mtx::crypto::sha256(
+                                        msg.key + this->canonical_json.dump()))) {
+                  this->cancelVerification(DeviceVerificationFlow::Error::MismatchedCommitment);
+                  return;
+              }
+          }
+
+          if (this->method == mtx::events::msg::SASMethods::Emoji) {
+              this->sasList = this->sas->generate_bytes_emoji(info);
+              setState(CompareEmoji);
+          } else if (this->method == mtx::events::msg::SASMethods::Decimal) {
+              this->sasList = this->sas->generate_bytes_decimal(info);
+              setState(CompareNumber);
+          }
+      });
+
+    connect(
+      ChatPage::instance(),
+      &ChatPage::receivedDeviceVerificationMac,
+      this,
+      [this](const mtx::events::msg::KeyVerificationMac &msg) {
+          nhlog::crypto()->info("verification: received mac");
+          if (msg.transaction_id.has_value()) {
+              if (msg.transaction_id.value() != this->transaction_id)
+                  return;
+          } else if (msg.relations.references()) {
+              if (msg.relations.references() != this->relation.event_id)
+                  return;
+          }
+
+          std::map<std::string, std::string> key_list;
+          std::string key_string;
+          for (const auto &mac : msg.mac) {
+              for (const auto &[deviceid, key] : their_keys.device_keys) {
+                  (void)deviceid;
+                  if (key.keys.count(mac.first))
+                      key_list[mac.first] = key.keys.at(mac.first);
+              }
+
+              if (their_keys.master_keys.keys.count(mac.first))
+                  key_list[mac.first] = their_keys.master_keys.keys[mac.first];
+              if (their_keys.user_signing_keys.keys.count(mac.first))
+                  key_list[mac.first] = their_keys.user_signing_keys.keys[mac.first];
+              if (their_keys.self_signing_keys.keys.count(mac.first))
+                  key_list[mac.first] = their_keys.self_signing_keys.keys[mac.first];
+          }
+          auto macs = key_verification_mac(sas.get(),
+                                           toClient,
+                                           this->deviceId.toStdString(),
+                                           http::client()->user_id(),
+                                           http::client()->device_id(),
+                                           this->transaction_id,
+                                           key_list);
+
+          for (const auto &[key, mac] : macs.mac) {
+              if (mac != msg.mac.at(key)) {
+                  this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
+                  return;
+              }
+          }
+
+          if (msg.keys == macs.keys) {
+              mtx::requests::KeySignaturesUpload req;
+              if (utils::localUser().toStdString() == this->toClient.to_string()) {
+                  // self verification, sign master key with device key, if we
+                  // verified it
+                  for (const auto &mac : msg.mac) {
+                      if (their_keys.master_keys.keys.count(mac.first)) {
+                          json j = their_keys.master_keys;
+                          j.erase("signatures");
+                          j.erase("unsigned");
+                          mtx::crypto::CrossSigningKeys master_key = j;
+                          master_key.signatures[utils::localUser().toStdString()]
+                                               ["ed25519:" + http::client()->device_id()] =
+                            olm::client()->sign_message(j.dump());
+                          req.signatures[utils::localUser().toStdString()]
+                                        [master_key.keys.at(mac.first)] = master_key;
+                      } else if (mac.first == "ed25519:" + this->deviceId.toStdString()) {
+                          // Sign their device key with self signing key
+
+                          auto device_id = this->deviceId.toStdString();
+
+                          if (their_keys.device_keys.count(device_id)) {
+                              json j = their_keys.device_keys.at(device_id);
+                              j.erase("signatures");
+                              j.erase("unsigned");
+
+                              auto secret = cache::secret(
+                                mtx::secret_storage::secrets::cross_signing_self_signing);
+                              if (!secret)
+                                  continue;
+                              auto ssk = mtx::crypto::PkSigning::from_seed(*secret);
+
+                              mtx::crypto::DeviceKeys dev = j;
+                              dev.signatures[utils::localUser().toStdString()]
+                                            ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
+
+                              req.signatures[utils::localUser().toStdString()][device_id] = dev;
+                          }
+                      }
+                  }
+              } else {
+                  // Sign their master key with user signing key
+                  for (const auto &mac : msg.mac) {
+                      if (their_keys.master_keys.keys.count(mac.first)) {
+                          json j = their_keys.master_keys;
+                          j.erase("signatures");
+                          j.erase("unsigned");
+
+                          auto secret =
+                            cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing);
+                          if (!secret)
+                              continue;
+                          auto usk = mtx::crypto::PkSigning::from_seed(*secret);
+
+                          mtx::crypto::CrossSigningKeys master_key = j;
+                          master_key.signatures[utils::localUser().toStdString()]
+                                               ["ed25519:" + usk.public_key()] = usk.sign(j.dump());
+
+                          req.signatures[toClient.to_string()][master_key.keys.at(mac.first)] =
+                            master_key;
+                      }
+                  }
+              }
+
+              if (!req.signatures.empty()) {
+                  http::client()->keys_signatures_upload(
+                    req,
+                    [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
+                        if (err) {
+                            nhlog::net()->error("failed to upload signatures: {},{}",
+                                                mtx::errors::to_string(err->matrix_error.errcode),
+                                                static_cast<int>(err->status_code));
+                        }
+
+                        for (const auto &[user_id, tmp] : res.errors)
+                            for (const auto &[key_id, e] : tmp)
+                                nhlog::net()->error("signature error for user {} and key "
+                                                    "id {}: {}, {}",
+                                                    user_id,
+                                                    key_id,
+                                                    mtx::errors::to_string(e.errcode),
+                                                    e.error);
+                    });
+              }
+
+              this->isMacVerified = true;
+              this->acceptDevice();
+          } else {
+              this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
+          }
+      });
+
+    connect(
+      ChatPage::instance(),
+      &ChatPage::receivedDeviceVerificationReady,
+      this,
+      [this](const mtx::events::msg::KeyVerificationReady &msg) {
+          nhlog::crypto()->info("verification: received ready");
+          if (!sender) {
+              if (msg.from_device != http::client()->device_id()) {
+                  error_ = User;
+                  emit errorChanged();
+                  setState(Failed);
+              }
+
+              return;
+          }
+
+          if (msg.transaction_id.has_value()) {
+              if (msg.transaction_id.value() != this->transaction_id)
+                  return;
+
+              if (this->deviceId.isEmpty() && this->deviceIds.size() > 1) {
+                  auto from = QString::fromStdString(msg.from_device);
+                  if (std::find(deviceIds.begin(), deviceIds.end(), from) != deviceIds.end()) {
+                      mtx::events::msg::KeyVerificationCancel req{};
+                      req.code           = "m.user";
+                      req.reason         = "accepted by other device";
+                      req.transaction_id = this->transaction_id;
+                      mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationCancel> body;
+
+                      for (const auto &d : this->deviceIds) {
+                          if (d != from)
+                              body[this->toClient][d.toStdString()] = req;
+                      }
+
+                      http::client()->send_to_device(
+                        http::client()->generate_txn_id(), body, [](mtx::http::RequestErr err) {
+                            if (err)
+                                nhlog::net()->warn(
+                                  "failed to send verification to_device message: {} {}",
+                                  err->matrix_error.error,
+                                  static_cast<int>(err->status_code));
+                        });
+
+                      this->deviceId  = from;
+                      this->deviceIds = {from};
+                  }
+              }
+          } else if (msg.relations.references()) {
+              if (msg.relations.references() != this->relation.event_id)
+                  return;
+              else {
+                  this->deviceId = QString::fromStdString(msg.from_device);
+              }
+          }
+          this->startVerificationRequest();
+      });
+
+    connect(ChatPage::instance(),
+            &ChatPage::receivedDeviceVerificationDone,
+            this,
+            [this](const mtx::events::msg::KeyVerificationDone &msg) {
+                nhlog::crypto()->info("verification: received done");
+                if (msg.transaction_id.has_value()) {
+                    if (msg.transaction_id.value() != this->transaction_id)
+                        return;
+                } else if (msg.relations.references()) {
+                    if (msg.relations.references() != this->relation.event_id)
+                        return;
+                }
+                nhlog::ui()->info("Flow done on other side");
+            });
+
+    timeout->start(TIMEOUT);
+}
+
+QString
+DeviceVerificationFlow::state()
+{
+    switch (state_) {
+    case PromptStartVerification:
+        return "PromptStartVerification";
+    case CompareEmoji:
+        return "CompareEmoji";
+    case CompareNumber:
+        return "CompareNumber";
+    case WaitingForKeys:
+        return "WaitingForKeys";
+    case WaitingForOtherToAccept:
+        return "WaitingForOtherToAccept";
+    case WaitingForMac:
+        return "WaitingForMac";
+    case Success:
+        return "Success";
+    case Failed:
+        return "Failed";
+    default:
+        return "";
+    }
+}
+
+void
+DeviceVerificationFlow::next()
+{
+    if (sender) {
+        switch (state_) {
+        case PromptStartVerification:
+            sendVerificationRequest();
+            break;
+        case CompareEmoji:
+        case CompareNumber:
+            sendVerificationMac();
+            break;
+        case WaitingForKeys:
+        case WaitingForOtherToAccept:
+        case WaitingForMac:
+        case Success:
+        case Failed:
+            nhlog::db()->error("verification: Invalid state transition!");
+            break;
+        }
+    } else {
+        switch (state_) {
+        case PromptStartVerification:
+            if (canonical_json.is_null())
+                sendVerificationReady();
+            else // legacy path without request and ready
+                acceptVerificationRequest();
+            break;
+        case CompareEmoji:
+            [[fallthrough]];
+        case CompareNumber:
+            sendVerificationMac();
+            break;
+        case WaitingForKeys:
+        case WaitingForOtherToAccept:
+        case WaitingForMac:
+        case Success:
+        case Failed:
+            nhlog::db()->error("verification: Invalid state transition!");
+            break;
+        }
+    }
+}
+
+QString
+DeviceVerificationFlow::getUserId()
+{
+    return QString::fromStdString(this->toClient.to_string());
+}
+
+QString
+DeviceVerificationFlow::getDeviceId()
+{
+    return this->deviceId;
+}
+
+bool
+DeviceVerificationFlow::getSender()
+{
+    return this->sender;
+}
+
+std::vector<int>
+DeviceVerificationFlow::getSasList()
+{
+    return this->sasList;
+}
+
+bool
+DeviceVerificationFlow::isSelfVerification() const
+{
+    return this->toClient.to_string() == http::client()->user_id().to_string();
+}
+
+void
+DeviceVerificationFlow::setEventId(std::string event_id_)
+{
+    this->relation.rel_type = mtx::common::RelationType::Reference;
+    this->relation.event_id = event_id_;
+    this->transaction_id    = event_id_;
+}
+
+void
+DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
+                                           std::string)
+{
+    if (msg.transaction_id.has_value()) {
+        if (msg.transaction_id.value() != this->transaction_id)
+            return;
+    } else if (msg.relations.references()) {
+        if (msg.relations.references() != this->relation.event_id)
+            return;
+    }
+    if ((std::find(msg.key_agreement_protocols.begin(),
+                   msg.key_agreement_protocols.end(),
+                   "curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
+        (std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
+        (std::find(msg.message_authentication_codes.begin(),
+                   msg.message_authentication_codes.end(),
+                   "hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
+        if (std::find(msg.short_authentication_string.begin(),
+                      msg.short_authentication_string.end(),
+                      mtx::events::msg::SASMethods::Emoji) !=
+            msg.short_authentication_string.end()) {
+            this->method = mtx::events::msg::SASMethods::Emoji;
+        } else if (std::find(msg.short_authentication_string.begin(),
+                             msg.short_authentication_string.end(),
+                             mtx::events::msg::SASMethods::Decimal) !=
+                   msg.short_authentication_string.end()) {
+            this->method = mtx::events::msg::SASMethods::Decimal;
+        } else {
+            this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
+            return;
+        }
+        if (!sender)
+            this->canonical_json = nlohmann::json(msg);
+        else {
+            if (utils::localUser().toStdString() < this->toClient.to_string()) {
+                this->canonical_json = nlohmann::json(msg);
+            }
+        }
+
+        if (state_ != PromptStartVerification)
+            this->acceptVerificationRequest();
+    } else {
+        this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
+    }
+}
+
+//! accepts a verification
+void
+DeviceVerificationFlow::acceptVerificationRequest()
+{
+    mtx::events::msg::KeyVerificationAccept req;
+
+    req.method                      = mtx::events::msg::VerificationMethods::SASv1;
+    req.key_agreement_protocol      = "curve25519-hkdf-sha256";
+    req.hash                        = "sha256";
+    req.message_authentication_code = "hkdf-hmac-sha256";
+    if (this->method == mtx::events::msg::SASMethods::Emoji)
+        req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
+    else if (this->method == mtx::events::msg::SASMethods::Decimal)
+        req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
+    req.commitment = mtx::crypto::bin2base64_unpadded(
+      mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
+
+    send(req);
+    setState(WaitingForKeys);
+}
+//! responds verification request
+void
+DeviceVerificationFlow::sendVerificationReady()
+{
+    mtx::events::msg::KeyVerificationReady req;
+
+    req.from_device = http::client()->device_id();
+    req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
+
+    send(req);
+    setState(WaitingForKeys);
+}
+//! accepts a verification
+void
+DeviceVerificationFlow::sendVerificationDone()
+{
+    mtx::events::msg::KeyVerificationDone req;
+
+    send(req);
+}
+//! starts the verification flow
+void
+DeviceVerificationFlow::startVerificationRequest()
+{
+    mtx::events::msg::KeyVerificationStart req;
+
+    req.from_device                  = http::client()->device_id();
+    req.method                       = mtx::events::msg::VerificationMethods::SASv1;
+    req.key_agreement_protocols      = {"curve25519-hkdf-sha256"};
+    req.hashes                       = {"sha256"};
+    req.message_authentication_codes = {"hkdf-hmac-sha256"};
+    req.short_authentication_string  = {mtx::events::msg::SASMethods::Decimal,
+                                       mtx::events::msg::SASMethods::Emoji};
+
+    if (this->type == DeviceVerificationFlow::Type::ToDevice) {
+        mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
+        req.transaction_id   = this->transaction_id;
+        this->canonical_json = nlohmann::json(req);
+    } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
+        req.relations.relations.push_back(this->relation);
+        // Set synthesized to surpress the nheko relation extensions
+        req.relations.synthesized = true;
+        this->canonical_json      = nlohmann::json(req);
+    }
+    send(req);
+    setState(WaitingForOtherToAccept);
+}
+//! sends a verification request
+void
+DeviceVerificationFlow::sendVerificationRequest()
+{
+    mtx::events::msg::KeyVerificationRequest req;
+
+    req.from_device = http::client()->device_id();
+    req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
+
+    if (this->type == DeviceVerificationFlow::Type::ToDevice) {
+        QDateTime currentTime = QDateTime::currentDateTimeUtc();
+
+        req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
+
+    } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
+        req.to      = this->toClient.to_string();
+        req.msgtype = "m.key.verification.request";
+        req.body    = "User is requesting to verify keys with you. However, your client does "
+                   "not support this method, so you will need to use the legacy method of "
+                   "key verification.";
+    }
+
+    send(req);
+    setState(WaitingForOtherToAccept);
+}
+//! cancels a verification flow
+void
+DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
+{
+    if (state_ == State::Success || state_ == State::Failed)
+        return;
+
+    mtx::events::msg::KeyVerificationCancel req;
+
+    if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
+        req.code   = "m.unknown_method";
+        req.reason = "unknown method received";
+    } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
+        req.code   = "m.mismatched_commitment";
+        req.reason = "commitment didn't match";
+    } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
+        req.code   = "m.mismatched_sas";
+        req.reason = "sas didn't match";
+    } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
+        req.code   = "m.key_match";
+        req.reason = "keys did not match";
+    } else if (error_code == DeviceVerificationFlow::Error::Timeout) {
+        req.code   = "m.timeout";
+        req.reason = "timed out";
+    } else if (error_code == DeviceVerificationFlow::Error::User) {
+        req.code   = "m.user";
+        req.reason = "user cancelled the verification";
+    } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
+        req.code   = "m.unexpected_message";
+        req.reason = "received messages out of order";
+    }
+
+    this->error_ = error_code;
+    emit errorChanged();
+    this->setState(Failed);
+
+    send(req);
+}
+//! sends the verification key
+void
+DeviceVerificationFlow::sendVerificationKey()
+{
+    mtx::events::msg::KeyVerificationKey req;
+
+    req.key = this->sas->public_key();
+
+    send(req);
+}
+
+mtx::events::msg::KeyVerificationMac
+key_verification_mac(mtx::crypto::SAS *sas,
+                     mtx::identifiers::User sender,
+                     const std::string &senderDevice,
+                     mtx::identifiers::User receiver,
+                     const std::string &receiverDevice,
+                     const std::string &transactionId,
+                     std::map<std::string, std::string> keys)
+{
+    mtx::events::msg::KeyVerificationMac req;
+
+    std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
+                       receiver.to_string() + receiverDevice + transactionId;
+
+    std::string key_list;
+    bool first = true;
+    for (const auto &[key_id, key] : keys) {
+        req.mac[key_id] = sas->calculate_mac(key, info + key_id);
+
+        if (!first)
+            key_list += ",";
+        key_list += key_id;
+        first = false;
+    }
+
+    req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
+
+    return req;
+}
+
+//! sends the mac of the keys
+void
+DeviceVerificationFlow::sendVerificationMac()
+{
+    std::map<std::string, std::string> key_list;
+    key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
+
+    // send our master key, if we trust it
+    if (!this->our_trusted_master_key.empty())
+        key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
+
+    mtx::events::msg::KeyVerificationMac req = key_verification_mac(sas.get(),
+                                                                    http::client()->user_id(),
+                                                                    http::client()->device_id(),
+                                                                    this->toClient,
+                                                                    this->deviceId.toStdString(),
+                                                                    this->transaction_id,
+                                                                    key_list);
+
+    send(req);
+
+    setState(WaitingForMac);
+    acceptDevice();
+}
+//! Completes the verification flow
+void
+DeviceVerificationFlow::acceptDevice()
+{
+    if (!isMacVerified) {
+        setState(WaitingForMac);
+    } else if (state_ == WaitingForMac) {
+        cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
+        this->sendVerificationDone();
+        setState(Success);
+
+        // Request secrets. We should probably check somehow, if a device knowns about the
+        // secrets.
+        if (utils::localUser().toStdString() == this->toClient.to_string() &&
+            (!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) ||
+             !cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) {
+            olm::request_cross_signing_keys();
+        }
+    }
+}
+
+void
+DeviceVerificationFlow::unverify()
+{
+    cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
+
+    emit refreshProfile();
+}
+
+QSharedPointer<DeviceVerificationFlow>
+DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
+                                              TimelineModel *timelineModel_,
+                                              const mtx::events::msg::KeyVerificationRequest &msg,
+                                              QString other_user_,
+                                              QString event_id_)
+{
+    QSharedPointer<DeviceVerificationFlow> flow(
+      new DeviceVerificationFlow(parent_,
+                                 Type::RoomMsg,
+                                 timelineModel_,
+                                 other_user_,
+                                 {QString::fromStdString(msg.from_device)}));
+
+    flow->setEventId(event_id_.toStdString());
+
+    if (std::find(msg.methods.begin(),
+                  msg.methods.end(),
+                  mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
+        flow->cancelVerification(UnknownMethod);
+    }
+
+    return flow;
+}
+QSharedPointer<DeviceVerificationFlow>
+DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
+                                                const mtx::events::msg::KeyVerificationRequest &msg,
+                                                QString other_user_,
+                                                QString txn_id_)
+{
+    QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
+      parent_, Type::ToDevice, nullptr, other_user_, {QString::fromStdString(msg.from_device)}));
+    flow->transaction_id = txn_id_.toStdString();
+
+    if (std::find(msg.methods.begin(),
+                  msg.methods.end(),
+                  mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
+        flow->cancelVerification(UnknownMethod);
+    }
+
+    return flow;
+}
+QSharedPointer<DeviceVerificationFlow>
+DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
+                                                const mtx::events::msg::KeyVerificationStart &msg,
+                                                QString other_user_,
+                                                QString txn_id_)
+{
+    QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
+      parent_, Type::ToDevice, nullptr, other_user_, {QString::fromStdString(msg.from_device)}));
+    flow->transaction_id = txn_id_.toStdString();
+
+    flow->handleStartMessage(msg, "");
+
+    return flow;
+}
+QSharedPointer<DeviceVerificationFlow>
+DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
+                                                 TimelineModel *timelineModel_,
+                                                 QString userid)
+{
+    QSharedPointer<DeviceVerificationFlow> flow(
+      new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, {}));
+    flow->sender = true;
+    return flow;
+}
+QSharedPointer<DeviceVerificationFlow>
+DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_,
+                                                   QString userid,
+                                                   std::vector<QString> devices)
+{
+    assert(!devices.empty());
+
+    QSharedPointer<DeviceVerificationFlow> flow(
+      new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, devices));
+
+    flow->sender         = true;
+    flow->transaction_id = http::client()->generate_txn_id();
+
+    return flow;
+}
diff --git a/src/encryption/DeviceVerificationFlow.h b/src/encryption/DeviceVerificationFlow.h
new file mode 100644
index 0000000000000000000000000000000000000000..537adf31bd8371f05854abb1276585e93b202c8f
--- /dev/null
+++ b/src/encryption/DeviceVerificationFlow.h
@@ -0,0 +1,251 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+
+#include <mtx/responses/crypto.hpp>
+#include <nlohmann/json.hpp>
+
+#include "CacheCryptoStructs.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "Olm.h"
+#include "timeline/TimelineModel.h"
+
+class QTimer;
+
+using sas_ptr = std::unique_ptr<mtx::crypto::SAS>;
+
+// clang-format off
+/*
+ * Stolen from fluffy chat :D
+ *
+ *      State         |   +-------------+                    +-----------+                                  |
+ *                    |   | AliceDevice |                    | BobDevice |                                  |
+ *                    |   | (sender)    |                    |           |                                  |
+ *                    |   +-------------+                    +-----------+                                  |
+ * promptStartVerify  |         |                                 |                                         |
+ *                    |      o  | (m.key.verification.request)    |                                         |
+ *                    |      p  |-------------------------------->| (ASK FOR VERIFICATION REQUEST)          |
+ * waitForOtherAccept |      t  |                                 |                                         | promptStartVerify
+ * &&                 |      i  |      (m.key.verification.ready) |                                         |
+ * no commitment      |      o  |<--------------------------------|                                         |
+ * &&                 |      n  |                                 |                                         |
+ * no canonical_json  |      a  |      (m.key.verification.start) |                                         | waitingForKeys
+ *                    |      l  |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
+ *                    |         |                                 |                                         | && no canonical_json
+ *                    |         | m.key.verification.start        |                                         |
+ * waitForOtherAccept |         |-------------------------------->| (IF NOT ALREADY ASKED,                  |
+ * &&                 |         |                                 |  ASK FOR VERIFICATION REQUEST)          | promptStartVerify, if not accepted
+ * canonical_json     |         |       m.key.verification.accept |                                         |
+ *                    |         |<--------------------------------|                                         |
+ * waitForOtherAccept |         |                                 |                                         | waitingForKeys
+ * &&                 |         | m.key.verification.key          |                                         | && canonical_json
+ * commitment         |         |-------------------------------->|                                         | && commitment
+ *                    |         |                                 |                                         |
+ *                    |         |          m.key.verification.key |                                         |
+ *                    |         |<--------------------------------|                                         |
+ * compareEmoji/Number|         |                                 |                                         | compareEmoji/Number
+ *                    |         |     COMPARE EMOJI / NUMBERS     |                                         |
+ *                    |         |                                 |                                         |
+ * waitingForMac      |         |     m.key.verification.mac      |                                         | waitingForMac
+ *                    | success |<------------------------------->|  success                                |
+ *                    |         |                                 |                                         |
+ * success/fail       |         |         m.key.verification.done |                                         | success/fail
+ *                    |         |<------------------------------->|                                         |
+ */
+// clang-format on
+class DeviceVerificationFlow : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString state READ state NOTIFY stateChanged)
+    Q_PROPERTY(Error error READ error NOTIFY errorChanged)
+    Q_PROPERTY(QString userId READ getUserId CONSTANT)
+    Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
+    Q_PROPERTY(bool sender READ getSender CONSTANT)
+    Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT)
+    Q_PROPERTY(bool isDeviceVerification READ isDeviceVerification CONSTANT)
+    Q_PROPERTY(bool isSelfVerification READ isSelfVerification CONSTANT)
+    Q_PROPERTY(bool isMultiDeviceVerification READ isMultiDeviceVerification CONSTANT)
+
+public:
+    enum State
+    {
+        PromptStartVerification,
+        WaitingForOtherToAccept,
+        WaitingForKeys,
+        CompareEmoji,
+        CompareNumber,
+        WaitingForMac,
+        Success,
+        Failed,
+    };
+    Q_ENUM(State)
+
+    enum Type
+    {
+        ToDevice,
+        RoomMsg
+    };
+
+    enum Error
+    {
+        UnknownMethod,
+        MismatchedCommitment,
+        MismatchedSAS,
+        KeyMismatch,
+        Timeout,
+        User,
+        OutOfOrder,
+    };
+    Q_ENUM(Error)
+
+    static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification(
+      QObject *parent_,
+      TimelineModel *timelineModel_,
+      const mtx::events::msg::KeyVerificationRequest &msg,
+      QString other_user_,
+      QString event_id_);
+    static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
+      QObject *parent_,
+      const mtx::events::msg::KeyVerificationRequest &msg,
+      QString other_user_,
+      QString txn_id_);
+    static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
+      QObject *parent_,
+      const mtx::events::msg::KeyVerificationStart &msg,
+      QString other_user_,
+      QString txn_id_);
+    static QSharedPointer<DeviceVerificationFlow>
+    InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
+    static QSharedPointer<DeviceVerificationFlow>
+    InitiateDeviceVerification(QObject *parent, QString userid, std::vector<QString> devices);
+
+    // getters
+    QString state();
+    Error error() { return error_; }
+    QString getUserId();
+    QString getDeviceId();
+    bool getSender();
+    std::vector<int> getSasList();
+    QString transactionId() { return QString::fromStdString(this->transaction_id); }
+    // setters
+    void setDeviceId(QString deviceID);
+    void setEventId(std::string event_id);
+    bool isDeviceVerification() const
+    {
+        return this->type == DeviceVerificationFlow::Type::ToDevice;
+    }
+    bool isSelfVerification() const;
+    bool isMultiDeviceVerification() const { return deviceIds.size() > 1; }
+
+    void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
+
+public slots:
+    //! unverifies a device
+    void unverify();
+    //! Continues the flow
+    void next();
+    //! Cancel the flow
+    void cancel() { cancelVerification(User); }
+
+signals:
+    void refreshProfile();
+    void stateChanged();
+    void errorChanged();
+
+private:
+    DeviceVerificationFlow(QObject *,
+                           DeviceVerificationFlow::Type flow_type,
+                           TimelineModel *model,
+                           QString userID,
+                           std::vector<QString> deviceIds_);
+    void setState(State state)
+    {
+        if (state != state_) {
+            state_ = state;
+            emit stateChanged();
+        }
+    }
+
+    void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
+    //! sends a verification request
+    void sendVerificationRequest();
+    //! accepts a verification request
+    void sendVerificationReady();
+    //! completes the verification flow();
+    void sendVerificationDone();
+    //! accepts a verification
+    void acceptVerificationRequest();
+    //! starts the verification flow
+    void startVerificationRequest();
+    //! cancels a verification flow
+    void cancelVerification(DeviceVerificationFlow::Error error_code);
+    //! sends the verification key
+    void sendVerificationKey();
+    //! sends the mac of the keys
+    void sendVerificationMac();
+    //! Completes the verification flow
+    void acceptDevice();
+
+    std::string transaction_id;
+
+    bool sender;
+    Type type;
+    mtx::identifiers::User toClient;
+    QString deviceId;
+    std::vector<QString> deviceIds;
+
+    // public part of our master key, when trusted or empty
+    std::string our_trusted_master_key;
+
+    mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
+    QTimer *timeout                     = nullptr;
+    sas_ptr sas;
+    std::string mac_method;
+    std::string commitment;
+    nlohmann::json canonical_json;
+
+    std::vector<int> sasList;
+    UserKeyCache their_keys;
+    TimelineModel *model_;
+    mtx::common::Relation relation;
+
+    State state_ = PromptStartVerification;
+    Error error_ = UnknownMethod;
+
+    bool isMacVerified = false;
+
+    template<typename T>
+    void send(T msg)
+    {
+        if (this->type == DeviceVerificationFlow::Type::ToDevice) {
+            mtx::requests::ToDeviceMessages<T> body;
+            msg.transaction_id = this->transaction_id;
+            for (const auto &d : deviceIds)
+                body[this->toClient][d.toStdString()] = msg;
+
+            http::client()->send_to_device<T>(
+              http::client()->generate_txn_id(), body, [](mtx::http::RequestErr err) {
+                  if (err)
+                      nhlog::net()->warn("failed to send verification to_device message: {} {}",
+                                         err->matrix_error.error,
+                                         static_cast<int>(err->status_code));
+              });
+        } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
+            if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>) {
+                msg.relations.relations.push_back(this->relation);
+                // Set synthesized to surpress the nheko relation extensions
+                msg.relations.synthesized = true;
+            }
+            (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
+        }
+
+        nhlog::net()->debug("Sent verification step: {} in state: {}",
+                            mtx::events::to_string(mtx::events::to_device_content_to_type<T>),
+                            state().toStdString());
+    }
+};
diff --git a/src/encryption/Olm.cpp b/src/encryption/Olm.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..01a16ba7cd2ad02a207c7a4aa0b062064e70c787
--- /dev/null
+++ b/src/encryption/Olm.cpp
@@ -0,0 +1,1629 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "Olm.h"
+
+#include <QObject>
+#include <QTimer>
+
+#include <nlohmann/json.hpp>
+#include <variant>
+
+#include <mtx/responses/common.hpp>
+#include <mtx/secret_storage.hpp>
+
+#include "Cache.h"
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "DeviceVerificationFlow.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "UserSettingsPage.h"
+#include "Utils.h"
+
+namespace {
+auto client_ = std::make_unique<mtx::crypto::OlmClient>();
+
+std::map<std::string, std::string> request_id_to_secret_name;
+
+constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
+}
+
+namespace olm {
+static void
+backup_session_key(const MegolmSessionIndex &idx,
+                   const GroupSessionData &data,
+                   mtx::crypto::InboundGroupSessionPtr &session);
+
+void
+from_json(const nlohmann::json &obj, OlmMessage &msg)
+{
+    if (obj.at("type") != "m.room.encrypted")
+        throw std::invalid_argument("invalid type for olm message");
+
+    if (obj.at("content").at("algorithm") != OLM_ALGO)
+        throw std::invalid_argument("invalid algorithm for olm message");
+
+    msg.sender     = obj.at("sender");
+    msg.sender_key = obj.at("content").at("sender_key");
+    msg.ciphertext = obj.at("content")
+                       .at("ciphertext")
+                       .get<std::map<std::string, mtx::events::msg::OlmCipherContent>>();
+}
+
+mtx::crypto::OlmClient *
+client()
+{
+    return client_.get();
+}
+
+static void
+handle_secret_request(const mtx::events::DeviceEvent<mtx::events::msg::SecretRequest> *e,
+                      const std::string &sender)
+{
+    using namespace mtx::events;
+
+    if (e->content.action != mtx::events::msg::RequestAction::Request)
+        return;
+
+    auto local_user = http::client()->user_id();
+
+    if (sender != local_user.to_string())
+        return;
+
+    auto verificationStatus = cache::verificationStatus(local_user.to_string());
+
+    if (!verificationStatus)
+        return;
+
+    auto deviceKeys = cache::userKeys(local_user.to_string());
+    if (!deviceKeys)
+        return;
+
+    if (std::find(verificationStatus->verified_devices.begin(),
+                  verificationStatus->verified_devices.end(),
+                  e->content.requesting_device_id) == verificationStatus->verified_devices.end())
+        return;
+
+    // this is a verified device
+    mtx::events::DeviceEvent<mtx::events::msg::SecretSend> secretSend;
+    secretSend.type               = EventType::SecretSend;
+    secretSend.content.request_id = e->content.request_id;
+
+    auto secret = cache::client()->secret(e->content.name);
+    if (!secret)
+        return;
+    secretSend.content.secret = secret.value();
+
+    send_encrypted_to_device_messages(
+      {{local_user.to_string(), {{e->content.requesting_device_id}}}}, secretSend);
+
+    nhlog::net()->info("Sent secret '{}' to ({},{})",
+                       e->content.name,
+                       local_user.to_string(),
+                       e->content.requesting_device_id);
+}
+
+void
+handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEvents> &msgs)
+{
+    if (msgs.empty())
+        return;
+    nhlog::crypto()->info("received {} to_device messages", msgs.size());
+    nlohmann::json j_msg;
+
+    for (const auto &msg : msgs) {
+        j_msg = std::visit([](auto &e) { return json(e); }, std::move(msg));
+        if (j_msg.count("type") == 0) {
+            nhlog::crypto()->warn("received message with no type field: {}", j_msg.dump(2));
+            continue;
+        }
+
+        std::string msg_type = j_msg.at("type");
+
+        if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) {
+            try {
+                olm::OlmMessage olm_msg = j_msg;
+                cache::client()->query_keys(
+                  olm_msg.sender, [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) {
+                      if (e) {
+                          nhlog::crypto()->error("Failed to query user keys, dropping olm "
+                                                 "message");
+                          return;
+                      }
+                      handle_olm_message(std::move(olm_msg), userKeys);
+                  });
+            } catch (const nlohmann::json::exception &e) {
+                nhlog::crypto()->warn(
+                  "parsing error for olm message: {} {}", e.what(), j_msg.dump(2));
+            } catch (const std::invalid_argument &e) {
+                nhlog::crypto()->warn(
+                  "validation error for olm message: {} {}", e.what(), j_msg.dump(2));
+            }
+
+        } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) {
+            nhlog::crypto()->warn("handling key request event: {}", j_msg.dump(2));
+            try {
+                mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> req = j_msg;
+                if (req.content.action == mtx::events::msg::RequestAction::Request)
+                    handle_key_request_message(req);
+                else
+                    nhlog::crypto()->warn("ignore key request (unhandled action): {}",
+                                          req.content.request_id);
+            } catch (const nlohmann::json::exception &e) {
+                nhlog::crypto()->warn(
+                  "parsing error for key_request message: {} {}", e.what(), j_msg.dump(2));
+            }
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationAccept>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationAccept(message.content);
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationRequest>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationRequest(message.content,
+                                                                    message.sender);
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationCancel>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationCancel(message.content);
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationKey>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationKey(message.content);
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationMac>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationMac(message.content);
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationStart>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationStart(message.content, message.sender);
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationReady>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationReady(message.content);
+        } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) {
+            auto message =
+              std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>(msg);
+            ChatPage::instance()->receivedDeviceVerificationDone(message.content);
+        } else if (auto e =
+                     std::get_if<mtx::events::DeviceEvent<mtx::events::msg::SecretRequest>>(&msg)) {
+            handle_secret_request(e, e->sender);
+        } else {
+            nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
+        }
+    }
+}
+
+void
+handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys)
+{
+    nhlog::crypto()->info("sender    : {}", msg.sender);
+    nhlog::crypto()->info("sender_key: {}", msg.sender_key);
+
+    if (msg.sender_key == olm::client()->identity_keys().ed25519) {
+        nhlog::crypto()->warn("Ignoring olm message from ourselves!");
+        return;
+    }
+
+    const auto my_key = olm::client()->identity_keys().curve25519;
+
+    bool failed_decryption = false;
+
+    for (const auto &cipher : msg.ciphertext) {
+        // We skip messages not meant for the current device.
+        if (cipher.first != my_key) {
+            nhlog::crypto()->debug(
+              "Skipping message for {} since we are {}.", cipher.first, my_key);
+            continue;
+        }
+
+        const auto type = cipher.second.type;
+        nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
+
+        auto payload = try_olm_decryption(msg.sender_key, cipher.second);
+
+        if (payload.is_null()) {
+            // Check for PRE_KEY message
+            if (cipher.second.type == 0) {
+                payload = handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second);
+            } else {
+                nhlog::crypto()->error("Undecryptable olm message!");
+                failed_decryption = true;
+                continue;
+            }
+        }
+
+        if (!payload.is_null()) {
+            mtx::events::collections::DeviceEvents device_event;
+
+            // Other properties are included in order to prevent an attacker from
+            // publishing someone else's curve25519 keys as their own and subsequently
+            // claiming to have sent messages which they didn't. sender must correspond
+            // to the user who sent the event, recipient to the local user, and
+            // recipient_keys to the local ed25519 key.
+            std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"];
+            if (receiver_ed25519.empty() ||
+                receiver_ed25519 != olm::client()->identity_keys().ed25519) {
+                nhlog::crypto()->warn("Decrypted event doesn't include our ed25519: {}",
+                                      payload.dump());
+                return;
+            }
+            std::string receiver = payload["recipient"];
+            if (receiver.empty() || receiver != http::client()->user_id().to_string()) {
+                nhlog::crypto()->warn("Decrypted event doesn't include our user_id: {}",
+                                      payload.dump());
+                return;
+            }
+
+            // Clients must confirm that the sender_key and the ed25519 field value
+            // under the keys property match the keys returned by /keys/query for the
+            // given user, and must also verify the signature of the payload. Without
+            // this check, a client cannot be sure that the sender device owns the
+            // private part of the ed25519 key it claims to have in the Olm payload.
+            // This is crucial when the ed25519 key corresponds to a verified device.
+            std::string sender_ed25519 = payload["keys"]["ed25519"];
+            if (sender_ed25519.empty()) {
+                nhlog::crypto()->warn("Decrypted event doesn't include sender ed25519: {}",
+                                      payload.dump());
+                return;
+            }
+
+            bool from_their_device = false;
+            for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
+                auto c_key = key.keys.find("curve25519:" + device_id);
+                auto e_key = key.keys.find("ed25519:" + device_id);
+
+                if (c_key == key.keys.end() || e_key == key.keys.end()) {
+                    nhlog::crypto()->warn("Skipping device {} as we have no keys for it.",
+                                          device_id);
+                } else if (c_key->second == msg.sender_key && e_key->second == sender_ed25519) {
+                    from_their_device = true;
+                    break;
+                }
+            }
+            if (!from_their_device) {
+                nhlog::crypto()->warn("Decrypted event isn't sent from a device "
+                                      "listed by that user! {}",
+                                      payload.dump());
+                return;
+            }
+
+            {
+                std::string msg_type = payload["type"];
+                json event_array     = json::array();
+                event_array.push_back(payload);
+
+                std::vector<mtx::events::collections::DeviceEvents> temp_events;
+                mtx::responses::utils::parse_device_events(event_array, temp_events);
+                if (temp_events.empty()) {
+                    nhlog::crypto()->warn("Decrypted unknown event: {}", payload.dump());
+                    return;
+                }
+                device_event = temp_events.at(0);
+            }
+
+            using namespace mtx::events;
+            if (auto e1 = std::get_if<DeviceEvent<msg::KeyVerificationAccept>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationAccept(e1->content);
+            } else if (auto e2 =
+                         std::get_if<DeviceEvent<msg::KeyVerificationRequest>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationRequest(e2->content, e2->sender);
+            } else if (auto e3 =
+                         std::get_if<DeviceEvent<msg::KeyVerificationCancel>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationCancel(e3->content);
+            } else if (auto e4 = std::get_if<DeviceEvent<msg::KeyVerificationKey>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationKey(e4->content);
+            } else if (auto e5 = std::get_if<DeviceEvent<msg::KeyVerificationMac>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationMac(e5->content);
+            } else if (auto e6 =
+                         std::get_if<DeviceEvent<msg::KeyVerificationStart>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationStart(e6->content, e6->sender);
+            } else if (auto e7 =
+                         std::get_if<DeviceEvent<msg::KeyVerificationReady>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationReady(e7->content);
+            } else if (auto e8 =
+                         std::get_if<DeviceEvent<msg::KeyVerificationDone>>(&device_event)) {
+                ChatPage::instance()->receivedDeviceVerificationDone(e8->content);
+            } else if (auto roomKey = std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) {
+                create_inbound_megolm_session(*roomKey, msg.sender_key, sender_ed25519);
+            } else if (auto forwardedRoomKey =
+                         std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(&device_event)) {
+                forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back(msg.sender_key);
+                import_inbound_megolm_session(*forwardedRoomKey);
+            } else if (auto e = std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) {
+                auto local_user = http::client()->user_id();
+
+                if (msg.sender != local_user.to_string())
+                    return;
+
+                auto secret_name = request_id_to_secret_name.find(e->content.request_id);
+
+                if (secret_name != request_id_to_secret_name.end()) {
+                    nhlog::crypto()->info("Received secret: {}", secret_name->second);
+
+                    mtx::events::msg::SecretRequest secretRequest{};
+                    secretRequest.action = mtx::events::msg::RequestAction::Cancellation;
+                    secretRequest.requesting_device_id = http::client()->device_id();
+                    secretRequest.request_id           = e->content.request_id;
+
+                    auto verificationStatus = cache::verificationStatus(local_user.to_string());
+
+                    if (!verificationStatus)
+                        return;
+
+                    auto deviceKeys = cache::userKeys(local_user.to_string());
+                    std::string sender_device_id;
+                    if (deviceKeys) {
+                        for (auto &[dev, key] : deviceKeys->device_keys) {
+                            if (key.keys["curve25519:" + dev] == msg.sender_key) {
+                                sender_device_id = dev;
+                                break;
+                            }
+                        }
+                    }
+
+                    std::map<mtx::identifiers::User,
+                             std::map<std::string, mtx::events::msg::SecretRequest>>
+                      body;
+
+                    for (const auto &dev : verificationStatus->verified_devices) {
+                        if (dev != secretRequest.requesting_device_id && dev != sender_device_id)
+                            body[local_user][dev] = secretRequest;
+                    }
+
+                    http::client()->send_to_device<mtx::events::msg::SecretRequest>(
+                      http::client()->generate_txn_id(),
+                      body,
+                      [name = secret_name->second](mtx::http::RequestErr err) {
+                          if (err) {
+                              nhlog::net()->error("Failed to send request cancellation "
+                                                  "for secrect "
+                                                  "'{}'",
+                                                  name);
+                          }
+                      });
+
+                    nhlog::crypto()->info("Storing secret {}", secret_name->second);
+                    cache::client()->storeSecret(secret_name->second, e->content.secret);
+
+                    request_id_to_secret_name.erase(secret_name);
+                }
+
+            } else if (auto sec_req = std::get_if<DeviceEvent<msg::SecretRequest>>(&device_event)) {
+                handle_secret_request(sec_req, msg.sender);
+            }
+
+            return;
+        } else {
+            failed_decryption = true;
+        }
+    }
+
+    if (failed_decryption) {
+        try {
+            std::map<std::string, std::vector<std::string>> targets;
+            for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
+                if (key.keys.at("curve25519:" + device_id) == msg.sender_key)
+                    targets[msg.sender].push_back(device_id);
+            }
+
+            send_encrypted_to_device_messages(
+              targets, mtx::events::DeviceEvent<mtx::events::msg::Dummy>{}, true);
+            nhlog::crypto()->info(
+              "Recovering from broken olm channel with {}:{}", msg.sender, msg.sender_key);
+        } catch (std::exception &e) {
+            nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", e.what());
+        }
+    }
+}
+
+nlohmann::json
+handle_pre_key_olm_message(const std::string &sender,
+                           const std::string &sender_key,
+                           const mtx::events::msg::OlmCipherContent &content)
+{
+    nhlog::crypto()->info("opening olm session with {}", sender);
+
+    mtx::crypto::OlmSessionPtr inbound_session = nullptr;
+    try {
+        inbound_session = olm::client()->create_inbound_session_from(sender_key, content.body);
+
+        // We also remove the one time key used to establish that
+        // session so we'll have to update our copy of the account object.
+        cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret()));
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to create inbound session with {}: {}", sender, e.what());
+        return {};
+    }
+
+    if (!mtx::crypto::matches_inbound_session_from(
+          inbound_session.get(), sender_key, content.body)) {
+        nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})", sender);
+        return {};
+    }
+
+    mtx::crypto::BinaryBuf output;
+    try {
+        output = olm::client()->decrypt_message(inbound_session.get(), content.type, content.body);
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to decrypt olm message {}: {}", content.body, e.what());
+        return {};
+    }
+
+    auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
+    nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2));
+
+    try {
+        nhlog::crypto()->debug("New olm session: {}",
+                               mtx::crypto::session_id(inbound_session.get()));
+        cache::saveOlmSession(
+          sender_key, std::move(inbound_session), QDateTime::currentMSecsSinceEpoch());
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("failed to save inbound olm session from {}: {}", sender, e.what());
+    }
+
+    return plaintext;
+}
+
+mtx::events::msg::Encrypted
+encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body)
+{
+    using namespace mtx::events;
+    using namespace mtx::identifiers;
+
+    auto own_user_id = http::client()->user_id().to_string();
+
+    auto members = cache::client()->getMembersWithKeys(
+      room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers());
+
+    std::map<std::string, std::vector<std::string>> sendSessionTo;
+    mtx::crypto::OutboundGroupSessionPtr session = nullptr;
+    GroupSessionData group_session_data;
+
+    if (cache::outboundMegolmSessionExists(room_id)) {
+        auto res                = cache::getOutboundMegolmSession(room_id);
+        auto encryptionSettings = cache::client()->roomEncryptionSettings(room_id);
+        mtx::events::state::Encryption defaultSettings;
+
+        // rotate if we crossed the limits for this key
+        if (res.data.message_index <
+              encryptionSettings.value_or(defaultSettings).rotation_period_msgs &&
+            (QDateTime::currentMSecsSinceEpoch() - res.data.timestamp) <
+              encryptionSettings.value_or(defaultSettings).rotation_period_ms) {
+            auto member_it             = members.begin();
+            auto session_member_it     = res.data.currently.keys.begin();
+            auto session_member_it_end = res.data.currently.keys.end();
+
+            while (member_it != members.end() || session_member_it != session_member_it_end) {
+                if (member_it == members.end()) {
+                    // a member left, purge session!
+                    nhlog::crypto()->debug("Rotating megolm session because of left member");
+                    break;
+                }
+
+                if (session_member_it == session_member_it_end) {
+                    // share with all remaining members
+                    while (member_it != members.end()) {
+                        sendSessionTo[member_it->first] = {};
+
+                        if (member_it->second)
+                            for (const auto &dev : member_it->second->device_keys)
+                                if (member_it->first != own_user_id || dev.first != device_id)
+                                    sendSessionTo[member_it->first].push_back(dev.first);
+
+                        ++member_it;
+                    }
+
+                    session = std::move(res.session);
+                    break;
+                }
+
+                if (member_it->first > session_member_it->first) {
+                    // a member left, purge session
+                    nhlog::crypto()->debug("Rotating megolm session because of left member");
+                    break;
+                } else if (member_it->first < session_member_it->first) {
+                    // new member, send them the session at this index
+                    sendSessionTo[member_it->first] = {};
+
+                    if (member_it->second) {
+                        for (const auto &dev : member_it->second->device_keys)
+                            if (member_it->first != own_user_id || dev.first != device_id)
+                                sendSessionTo[member_it->first].push_back(dev.first);
+                    }
+
+                    ++member_it;
+                } else {
+                    // compare devices
+                    bool device_removed = false;
+                    for (const auto &dev : session_member_it->second.deviceids) {
+                        if (!member_it->second ||
+                            !member_it->second->device_keys.count(dev.first)) {
+                            device_removed = true;
+                            break;
+                        }
+                    }
+
+                    if (device_removed) {
+                        // device removed, rotate session!
+                        nhlog::crypto()->debug("Rotating megolm session because of removed "
+                                               "device of {}",
+                                               member_it->first);
+                        break;
+                    }
+
+                    // check for new devices to share with
+                    if (member_it->second)
+                        for (const auto &dev : member_it->second->device_keys)
+                            if (!session_member_it->second.deviceids.count(dev.first) &&
+                                (member_it->first != own_user_id || dev.first != device_id))
+                                sendSessionTo[member_it->first].push_back(dev.first);
+
+                    ++member_it;
+                    ++session_member_it;
+                    if (member_it == members.end() && session_member_it == session_member_it_end) {
+                        // all devices match or are newly added
+                        session = std::move(res.session);
+                    }
+                }
+            }
+        }
+
+        group_session_data = std::move(res.data);
+    }
+
+    if (!session) {
+        nhlog::ui()->debug("creating new outbound megolm session");
+
+        // Create a new outbound megolm session.
+        session                = olm::client()->init_outbound_group_session();
+        const auto session_id  = mtx::crypto::session_id(session.get());
+        const auto session_key = mtx::crypto::session_key(session.get());
+
+        // Saving the new megolm session.
+        GroupSessionData session_data{};
+        session_data.message_index              = 0;
+        session_data.timestamp                  = QDateTime::currentMSecsSinceEpoch();
+        session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519;
+
+        sendSessionTo.clear();
+
+        for (const auto &[user, devices] : members) {
+            sendSessionTo[user]               = {};
+            session_data.currently.keys[user] = {};
+            if (devices) {
+                for (const auto &[device_id_, key] : devices->device_keys) {
+                    (void)key;
+                    if (device_id != device_id_ || user != own_user_id) {
+                        sendSessionTo[user].push_back(device_id_);
+                        session_data.currently.keys[user].deviceids[device_id_] = 0;
+                    }
+                }
+            }
+        }
+
+        {
+            MegolmSessionIndex index;
+            index.room_id       = room_id;
+            index.session_id    = session_id;
+            index.sender_key    = olm::client()->identity_keys().curve25519;
+            auto megolm_session = olm::client()->init_inbound_group_session(session_key);
+            backup_session_key(index, session_data, megolm_session);
+            cache::saveInboundMegolmSession(index, std::move(megolm_session), session_data);
+        }
+
+        cache::saveOutboundMegolmSession(room_id, session_data, session);
+        group_session_data = std::move(session_data);
+    }
+
+    mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload{};
+    megolm_payload.content.algorithm   = MEGOLM_ALGO;
+    megolm_payload.content.room_id     = room_id;
+    megolm_payload.content.session_id  = mtx::crypto::session_id(session.get());
+    megolm_payload.content.session_key = mtx::crypto::session_key(session.get());
+    megolm_payload.type                = mtx::events::EventType::RoomKey;
+
+    if (!sendSessionTo.empty())
+        olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload);
+
+    // relations shouldn't be encrypted...
+    mtx::common::Relations relations = mtx::common::parse_relations(body["content"]);
+
+    auto payload = olm::client()->encrypt_group_message(session.get(), body.dump());
+
+    // Prepare the m.room.encrypted event.
+    msg::Encrypted data;
+    data.ciphertext = std::string((char *)payload.data(), payload.size());
+    data.sender_key = olm::client()->identity_keys().curve25519;
+    data.session_id = mtx::crypto::session_id(session.get());
+    data.device_id  = device_id;
+    data.algorithm  = MEGOLM_ALGO;
+    data.relations  = relations;
+
+    group_session_data.message_index = olm_outbound_group_session_message_index(session.get());
+    nhlog::crypto()->debug("next message_index {}", group_session_data.message_index);
+
+    // update current set of members for the session with the new members and that message_index
+    for (const auto &[user, devices] : sendSessionTo) {
+        if (!group_session_data.currently.keys.count(user))
+            group_session_data.currently.keys[user] = {};
+
+        for (const auto &device_id_ : devices) {
+            if (!group_session_data.currently.keys[user].deviceids.count(device_id_))
+                group_session_data.currently.keys[user].deviceids[device_id_] =
+                  group_session_data.message_index;
+        }
+    }
+
+    // We need to re-pickle the session after we send a message to save the new message_index.
+    cache::updateOutboundMegolmSession(room_id, group_session_data, session);
+
+    return data;
+}
+
+nlohmann::json
+try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &msg)
+{
+    auto session_ids = cache::getOlmSessions(sender_key);
+
+    nhlog::crypto()->info("attempt to decrypt message with {} known session_ids",
+                          session_ids.size());
+
+    for (const auto &id : session_ids) {
+        auto session = cache::getOlmSession(sender_key, id);
+
+        if (!session) {
+            nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id);
+            continue;
+        }
+
+        mtx::crypto::BinaryBuf text;
+
+        try {
+            text = olm::client()->decrypt_message(session->get(), msg.type, msg.body);
+            nhlog::crypto()->debug("Updated olm session: {}",
+                                   mtx::crypto::session_id(session->get()));
+            cache::saveOlmSession(
+              id, std::move(session.value()), QDateTime::currentMSecsSinceEpoch());
+        } catch (const mtx::crypto::olm_exception &e) {
+            nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}",
+                                   msg.type,
+                                   sender_key,
+                                   id,
+                                   e.what());
+            continue;
+        } catch (const lmdb::error &e) {
+            nhlog::crypto()->critical("failed to save session: {}", e.what());
+            return {};
+        }
+
+        try {
+            return json::parse(std::string_view((char *)text.data(), text.size()));
+        } catch (const json::exception &e) {
+            nhlog::crypto()->critical("failed to parse the decrypted session msg: {} {}",
+                                      e.what(),
+                                      std::string_view((char *)text.data(), text.size()));
+        }
+    }
+
+    return {};
+}
+
+void
+create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
+                              const std::string &sender_key,
+                              const std::string &sender_ed25519)
+{
+    MegolmSessionIndex index;
+    index.room_id    = roomKey.content.room_id;
+    index.session_id = roomKey.content.session_id;
+    index.sender_key = sender_key;
+
+    try {
+        GroupSessionData data{};
+        data.forwarding_curve25519_key_chain = {sender_key};
+        data.sender_claimed_ed25519_key      = sender_ed25519;
+
+        auto megolm_session =
+          olm::client()->init_inbound_group_session(roomKey.content.session_key);
+        backup_session_key(index, data, megolm_session);
+        cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
+    } catch (const lmdb::error &e) {
+        nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
+        return;
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what());
+        return;
+    }
+
+    nhlog::crypto()->info(
+      "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
+
+    ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
+}
+
+void
+import_inbound_megolm_session(
+  const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey)
+{
+    MegolmSessionIndex index;
+    index.room_id    = roomKey.content.room_id;
+    index.session_id = roomKey.content.session_id;
+    index.sender_key = roomKey.content.sender_key;
+
+    try {
+        auto megolm_session =
+          olm::client()->import_inbound_group_session(roomKey.content.session_key);
+
+        GroupSessionData data{};
+        data.forwarding_curve25519_key_chain = roomKey.content.forwarding_curve25519_key_chain;
+        data.sender_claimed_ed25519_key      = roomKey.content.sender_claimed_ed25519_key;
+        // may have come from online key backup, so we can't trust it...
+        data.trusted = false;
+        // if we got it forwarded from the sender, assume it is trusted. They may still have
+        // used key backup, but it is unlikely.
+        if (roomKey.content.forwarding_curve25519_key_chain.size() == 1 &&
+            roomKey.content.forwarding_curve25519_key_chain.back() == roomKey.content.sender_key) {
+            data.trusted = true;
+        }
+
+        backup_session_key(index, data, megolm_session);
+        cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
+    } catch (const lmdb::error &e) {
+        nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
+        return;
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what());
+        return;
+    }
+
+    nhlog::crypto()->info(
+      "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
+
+    ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
+}
+
+void
+backup_session_key(const MegolmSessionIndex &idx,
+                   const GroupSessionData &data,
+                   mtx::crypto::InboundGroupSessionPtr &session)
+{
+    try {
+        if (!UserSettings::instance()->useOnlineKeyBackup()) {
+            // Online key backup disabled
+            return;
+        }
+
+        auto backupVersion = cache::client()->backupVersion();
+        if (!backupVersion) {
+            // no trusted OKB
+            return;
+        }
+
+        using namespace mtx::crypto;
+
+        auto decryptedSecret = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
+        if (!decryptedSecret) {
+            // no backup key available
+            return;
+        }
+        auto sessionDecryptionKey = to_binary_buf(base642bin(*decryptedSecret));
+
+        auto public_key = mtx::crypto::CURVE25519_public_key_from_private(sessionDecryptionKey);
+
+        mtx::responses::backup::SessionData sessionData;
+        sessionData.algorithm                       = mtx::crypto::MEGOLM_ALGO;
+        sessionData.forwarding_curve25519_key_chain = data.forwarding_curve25519_key_chain;
+        sessionData.sender_claimed_keys["ed25519"]  = data.sender_claimed_ed25519_key;
+        sessionData.sender_key                      = idx.sender_key;
+        sessionData.session_key = mtx::crypto::export_session(session.get(), -1);
+
+        auto encrypt_session = mtx::crypto::encrypt_session(sessionData, public_key);
+
+        mtx::responses::backup::SessionBackup bk;
+        bk.first_message_index = olm_inbound_group_session_first_known_index(session.get());
+        bk.forwarded_count     = data.forwarding_curve25519_key_chain.size();
+        bk.is_verified         = false;
+        bk.session_data        = std::move(encrypt_session);
+
+        http::client()->put_room_keys(
+          backupVersion->version,
+          idx.room_id,
+          idx.session_id,
+          bk,
+          [idx](mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::net()->warn("failed to backup session key ({}:{}): {} ({})",
+                                     idx.room_id,
+                                     idx.session_id,
+                                     err->matrix_error.error,
+                                     static_cast<int>(err->status_code));
+              } else {
+                  nhlog::crypto()->debug(
+                    "backed up session key ({}:{})", idx.room_id, idx.session_id);
+              }
+          });
+    } catch (std::exception &e) {
+        nhlog::net()->warn("failed to backup session key: {}", e.what());
+    }
+}
+
+void
+mark_keys_as_published()
+{
+    olm::client()->mark_keys_as_published();
+    cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret()));
+}
+
+void
+lookup_keybackup(const std::string room, const std::string session_id)
+{
+    if (!UserSettings::instance()->useOnlineKeyBackup()) {
+        // Online key backup disabled
+        return;
+    }
+
+    auto backupVersion = cache::client()->backupVersion();
+    if (!backupVersion) {
+        // no trusted OKB
+        return;
+    }
+
+    using namespace mtx::crypto;
+
+    auto decryptedSecret = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
+    if (!decryptedSecret) {
+        // no backup key available
+        return;
+    }
+    auto sessionDecryptionKey = to_binary_buf(base642bin(*decryptedSecret));
+
+    http::client()->room_keys(
+      backupVersion->version,
+      room,
+      session_id,
+      [room, session_id, sessionDecryptionKey](const mtx::responses::backup::SessionBackup &bk,
+                                               mtx::http::RequestErr err) {
+          if (err) {
+              if (err->status_code != 404)
+                  nhlog::crypto()->error("Failed to dowload key {}:{}: {} - {}",
+                                         room,
+                                         session_id,
+                                         mtx::errors::to_string(err->matrix_error.errcode),
+                                         err->matrix_error.error);
+              return;
+          }
+          try {
+              auto session = decrypt_session(bk.session_data, sessionDecryptionKey);
+
+              if (session.algorithm != mtx::crypto::MEGOLM_ALGO)
+                  // don't know this algorithm
+                  return;
+
+              MegolmSessionIndex index;
+              index.room_id    = room;
+              index.session_id = session_id;
+              index.sender_key = session.sender_key;
+
+              GroupSessionData data{};
+              data.forwarding_curve25519_key_chain = session.forwarding_curve25519_key_chain;
+              data.sender_claimed_ed25519_key      = session.sender_claimed_keys["ed25519"];
+              // online key backup can't be trusted, because anyone can upload to it.
+              data.trusted = false;
+
+              auto megolm_session =
+                olm::client()->import_inbound_group_session(session.session_key);
+
+              if (!cache::inboundMegolmSessionExists(index) ||
+                  olm_inbound_group_session_first_known_index(megolm_session.get()) <
+                    olm_inbound_group_session_first_known_index(
+                      cache::getInboundMegolmSession(index).get())) {
+                  cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
+
+                  nhlog::crypto()->info("imported inbound megolm session "
+                                        "from key backup ({}, {})",
+                                        room,
+                                        session_id);
+
+                  // call on UI thread
+                  QTimer::singleShot(0, ChatPage::instance(), [index] {
+                      ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
+                  });
+              }
+          } catch (const lmdb::error &e) {
+              nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
+              return;
+          } catch (const mtx::crypto::olm_exception &e) {
+              nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what());
+              return;
+          }
+      });
+}
+
+void
+send_key_request_for(mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> e,
+                     const std::string &request_id,
+                     bool cancel)
+{
+    using namespace mtx::events;
+
+    nhlog::crypto()->debug("sending key request: sender_key {}, session_id {}",
+                           e.content.sender_key,
+                           e.content.session_id);
+
+    mtx::events::msg::KeyRequest request;
+    request.action = cancel ? mtx::events::msg::RequestAction::Cancellation
+                            : mtx::events::msg::RequestAction::Request;
+
+    request.algorithm            = MEGOLM_ALGO;
+    request.room_id              = e.room_id;
+    request.sender_key           = e.content.sender_key;
+    request.session_id           = e.content.session_id;
+    request.request_id           = request_id;
+    request.requesting_device_id = http::client()->device_id();
+
+    nhlog::crypto()->debug("m.room_key_request: {}", json(request).dump(2));
+
+    std::map<mtx::identifiers::User, std::map<std::string, decltype(request)>> body;
+    body[mtx::identifiers::parse<mtx::identifiers::User>(e.sender)][e.content.device_id] = request;
+    body[http::client()->user_id()]["*"]                                                 = request;
+
+    http::client()->send_to_device(
+      http::client()->generate_txn_id(), body, [e](mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to send "
+                                 "send_to_device "
+                                 "message: {}",
+                                 err->matrix_error.error);
+          }
+
+          nhlog::net()->info(
+            "m.room_key_request sent to {}:{} and your own devices", e.sender, e.content.device_id);
+      });
+
+    // http::client()->room_keys
+}
+
+void
+handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> &req)
+{
+    if (req.content.algorithm != MEGOLM_ALGO) {
+        nhlog::crypto()->debug("ignoring key request {} with invalid algorithm: {}",
+                               req.content.request_id,
+                               req.content.algorithm);
+        return;
+    }
+
+    // Check if we were the sender of the session being requested (unless it is actually us
+    // requesting the session).
+    if (req.sender != http::client()->user_id().to_string() &&
+        req.content.sender_key != olm::client()->identity_keys().curve25519) {
+        nhlog::crypto()->debug(
+          "ignoring key request {} because we did not create the requested session: "
+          "\nrequested({}) ours({})",
+          req.content.request_id,
+          req.content.sender_key,
+          olm::client()->identity_keys().curve25519);
+        return;
+    }
+
+    // Check that the requested session_id and the one we have saved match.
+    MegolmSessionIndex index{};
+    index.room_id    = req.content.room_id;
+    index.session_id = req.content.session_id;
+    index.sender_key = req.content.sender_key;
+
+    // Check if we have the keys for the requested session.
+    auto sessionData = cache::getMegolmSessionData(index);
+    if (!sessionData) {
+        nhlog::crypto()->warn("requested session not found in room: {}", req.content.room_id);
+        return;
+    }
+
+    const auto session = cache::getInboundMegolmSession(index);
+    if (!session) {
+        nhlog::crypto()->warn("No session with id {} in db", req.content.session_id);
+        return;
+    }
+
+    if (!cache::isRoomMember(req.sender, req.content.room_id)) {
+        nhlog::crypto()->warn("user {} that requested the session key is not member of the room {}",
+                              req.sender,
+                              req.content.room_id);
+        return;
+    }
+
+    // check if device is verified
+    auto verificationStatus = cache::verificationStatus(req.sender);
+    bool verifiedDevice     = false;
+    if (verificationStatus &&
+        // Share keys, if the option to share with trusted users is enabled or with yourself
+        (ChatPage::instance()->userSettings()->shareKeysWithTrustedUsers() ||
+         req.sender == http::client()->user_id().to_string())) {
+        for (const auto &dev : verificationStatus->verified_devices) {
+            if (dev == req.content.requesting_device_id) {
+                verifiedDevice = true;
+                nhlog::crypto()->debug("Verified device: {}", dev);
+                break;
+            }
+        }
+    }
+
+    bool shouldSeeKeys    = false;
+    uint64_t minimumIndex = -1;
+    if (sessionData->currently.keys.count(req.sender)) {
+        if (sessionData->currently.keys.at(req.sender)
+              .deviceids.count(req.content.requesting_device_id)) {
+            shouldSeeKeys = true;
+            minimumIndex  = sessionData->currently.keys.at(req.sender)
+                             .deviceids.at(req.content.requesting_device_id);
+        }
+    }
+
+    if (!verifiedDevice && !shouldSeeKeys) {
+        nhlog::crypto()->debug("ignoring key request for room {}", req.content.room_id);
+        return;
+    }
+
+    if (verifiedDevice) {
+        // share the minimum index we have
+        minimumIndex = -1;
+    }
+
+    try {
+        auto session_key = mtx::crypto::export_session(session.get(), minimumIndex);
+
+        //
+        // Prepare the m.room_key event.
+        //
+        mtx::events::msg::ForwardedRoomKey forward_key{};
+        forward_key.algorithm   = MEGOLM_ALGO;
+        forward_key.room_id     = index.room_id;
+        forward_key.session_id  = index.session_id;
+        forward_key.session_key = session_key;
+        forward_key.sender_key  = index.sender_key;
+
+        // TODO(Nico): Figure out if this is correct
+        forward_key.sender_claimed_ed25519_key      = sessionData->sender_claimed_ed25519_key;
+        forward_key.forwarding_curve25519_key_chain = sessionData->forwarding_curve25519_key_chain;
+
+        send_megolm_key_to_device(req.sender, req.content.requesting_device_id, forward_key);
+    } catch (std::exception &e) {
+        nhlog::crypto()->error("Failed to forward session key: {}", e.what());
+    }
+}
+
+void
+send_megolm_key_to_device(const std::string &user_id,
+                          const std::string &device_id,
+                          const mtx::events::msg::ForwardedRoomKey &payload)
+{
+    mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> room_key;
+    room_key.content = payload;
+    room_key.type    = mtx::events::EventType::ForwardedRoomKey;
+
+    std::map<std::string, std::vector<std::string>> targets;
+    targets[user_id] = {device_id};
+    send_encrypted_to_device_messages(targets, room_key);
+    nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id);
+}
+
+DecryptionResult
+decryptEvent(const MegolmSessionIndex &index,
+             const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event,
+             bool dont_write_db)
+{
+    try {
+        if (!cache::client()->inboundMegolmSessionExists(index)) {
+            return {DecryptionErrorCode::MissingSession, std::nullopt, std::nullopt};
+        }
+    } catch (const lmdb::error &e) {
+        return {DecryptionErrorCode::DbError, e.what(), std::nullopt};
+    }
+
+    // TODO: Lookup index,event_id,origin_server_ts tuple for replay attack errors
+
+    std::string msg_str;
+    try {
+        auto session = cache::client()->getInboundMegolmSession(index);
+        if (!session) {
+            return {DecryptionErrorCode::MissingSession, std::nullopt, std::nullopt};
+        }
+
+        auto sessionData =
+          cache::client()->getMegolmSessionData(index).value_or(GroupSessionData{});
+
+        auto res = olm::client()->decrypt_group_message(session.get(), event.content.ciphertext);
+        msg_str  = std::string((char *)res.data.data(), res.data.size());
+
+        if (!event.event_id.empty() && event.event_id[0] == '$') {
+            auto oldIdx = sessionData.indices.find(res.message_index);
+            if (oldIdx != sessionData.indices.end()) {
+                if (oldIdx->second != event.event_id)
+                    return {DecryptionErrorCode::ReplayAttack, std::nullopt, std::nullopt};
+            } else if (!dont_write_db) {
+                sessionData.indices[res.message_index] = event.event_id;
+                cache::client()->saveInboundMegolmSession(index, std::move(session), sessionData);
+            }
+        }
+    } catch (const lmdb::error &e) {
+        return {DecryptionErrorCode::DbError, e.what(), std::nullopt};
+    } catch (const mtx::crypto::olm_exception &e) {
+        if (e.error_code() == mtx::crypto::OlmErrorCode::UNKNOWN_MESSAGE_INDEX)
+            return {DecryptionErrorCode::MissingSessionIndex, e.what(), std::nullopt};
+        return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt};
+    }
+
+    try {
+        // Add missing fields for the event.
+        json body                = json::parse(msg_str);
+        body["event_id"]         = event.event_id;
+        body["sender"]           = event.sender;
+        body["origin_server_ts"] = event.origin_server_ts;
+        body["unsigned"]         = event.unsigned_data;
+
+        // relations are unencrypted in content...
+        mtx::common::add_relations(body["content"], event.content.relations);
+
+        mtx::events::collections::TimelineEvent te;
+        mtx::events::collections::from_json(body, te);
+
+        return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)};
+    } catch (std::exception &e) {
+        return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
+    }
+}
+
+crypto::Trust
+calculate_trust(const std::string &user_id, const MegolmSessionIndex &index)
+{
+    auto status              = cache::client()->verificationStatus(user_id);
+    auto megolmData          = cache::client()->getMegolmSessionData(index);
+    crypto::Trust trustlevel = crypto::Trust::Unverified;
+
+    if (megolmData && megolmData->trusted && status.verified_device_keys.count(index.sender_key))
+        trustlevel = status.verified_device_keys.at(index.sender_key);
+
+    return trustlevel;
+}
+
+//! Send encrypted to device messages, targets is a map from userid to device ids or {} for all
+//! devices
+void
+send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::string>> targets,
+                                  const mtx::events::collections::DeviceEvents &event,
+                                  bool force_new_session)
+{
+    static QMap<QPair<std::string, std::string>, qint64> rateLimit;
+
+    nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event);
+
+    std::map<std::string, std::vector<std::string>> keysToQuery;
+    mtx::requests::ClaimKeys claims;
+    std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::OlmEncrypted>>
+      messages;
+    std::map<std::string, std::map<std::string, DevicePublicKeys>> pks;
+
+    auto our_curve = olm::client()->identity_keys().curve25519;
+
+    for (const auto &[user, devices] : targets) {
+        auto deviceKeys = cache::client()->userKeys(user);
+
+        // no keys for user, query them
+        if (!deviceKeys) {
+            keysToQuery[user] = devices;
+            continue;
+        }
+
+        auto deviceTargets = devices;
+        if (devices.empty()) {
+            deviceTargets.clear();
+            for (const auto &[device, keys] : deviceKeys->device_keys) {
+                (void)keys;
+                deviceTargets.push_back(device);
+            }
+        }
+
+        for (const auto &device : deviceTargets) {
+            if (!deviceKeys->device_keys.count(device)) {
+                keysToQuery[user] = {};
+                break;
+            }
+
+            auto d = deviceKeys->device_keys.at(device);
+
+            if (!d.keys.count("curve25519:" + device) || !d.keys.count("ed25519:" + device)) {
+                nhlog::crypto()->warn("Skipping device {} since it has no keys!", device);
+                continue;
+            }
+
+            auto device_curve = d.keys.at("curve25519:" + device);
+            if (device_curve == our_curve) {
+                nhlog::crypto()->warn("Skipping our own device, since sending "
+                                      "ourselves olm messages makes no sense.");
+                continue;
+            }
+
+            auto session = cache::getLatestOlmSession(device_curve);
+            if (!session || force_new_session) {
+                auto currentTime = QDateTime::currentSecsSinceEpoch();
+                if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < currentTime) {
+                    claims.one_time_keys[user][device] = mtx::crypto::SIGNED_CURVE25519;
+                    pks[user][device].ed25519          = d.keys.at("ed25519:" + device);
+                    pks[user][device].curve25519       = d.keys.at("curve25519:" + device);
+
+                    rateLimit.insert(QPair(user, device), currentTime);
+                } else {
+                    nhlog::crypto()->warn("Not creating new session with {}:{} "
+                                          "because of rate limit",
+                                          user,
+                                          device);
+                }
+                continue;
+            }
+
+            messages[mtx::identifiers::parse<mtx::identifiers::User>(user)][device] =
+              olm::client()
+                ->create_olm_encrypted_content(session->get(),
+                                               ev_json,
+                                               UserId(user),
+                                               d.keys.at("ed25519:" + device),
+                                               device_curve)
+                .get<mtx::events::msg::OlmEncrypted>();
+
+            try {
+                nhlog::crypto()->debug("Updated olm session: {}",
+                                       mtx::crypto::session_id(session->get()));
+                cache::saveOlmSession(d.keys.at("curve25519:" + device),
+                                      std::move(*session),
+                                      QDateTime::currentMSecsSinceEpoch());
+            } 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());
+            }
+        }
+    }
+
+    if (!messages.empty())
+        http::client()->send_to_device<mtx::events::msg::OlmEncrypted>(
+          http::client()->generate_txn_id(), messages, [](mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::net()->warn("failed to send "
+                                     "send_to_device "
+                                     "message: {}",
+                                     err->matrix_error.error);
+              }
+          });
+
+    auto BindPks = [ev_json](decltype(pks) pks_temp) {
+        return [pks = pks_temp, ev_json](const mtx::responses::ClaimKeys &res,
+                                         mtx::http::RequestErr) {
+            std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::OlmEncrypted>>
+              messages;
+            for (const auto &[user_id, retrieved_devices] : res.one_time_keys) {
+                nhlog::net()->debug("claimed keys for {}", user_id);
+                if (retrieved_devices.size() == 0) {
+                    nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
+                    continue;
+                }
+
+                for (const auto &rd : retrieved_devices) {
+                    const auto device_id = rd.first;
+
+                    nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
+
+                    if (rd.second.empty() || !rd.second.begin()->contains("key")) {
+                        nhlog::net()->warn("Skipping device {} as it has no key.", device_id);
+                        continue;
+                    }
+
+                    auto otk = rd.second.begin()->at("key");
+
+                    auto sign_key = pks.at(user_id).at(device_id).ed25519;
+                    auto id_key   = pks.at(user_id).at(device_id).curve25519;
+
+                    // Verify signature
+                    {
+                        auto signedKey = *rd.second.begin();
+                        std::string signature =
+                          signedKey["signatures"][user_id].value("ed25519:" + device_id, "");
+
+                        if (signature.empty() || !mtx::crypto::ed25519_verify_signature(
+                                                   sign_key, signedKey, signature)) {
+                            nhlog::net()->warn("Skipping device {} as its one time key "
+                                               "has an invalid signature.",
+                                               device_id);
+                            continue;
+                        }
+                    }
+
+                    auto session = olm::client()->create_outbound_session(id_key, otk);
+
+                    messages[mtx::identifiers::parse<mtx::identifiers::User>(user_id)][device_id] =
+                      olm::client()
+                        ->create_olm_encrypted_content(
+                          session.get(), ev_json, UserId(user_id), sign_key, id_key)
+                        .get<mtx::events::msg::OlmEncrypted>();
+
+                    try {
+                        nhlog::crypto()->debug("Updated olm session: {}",
+                                               mtx::crypto::session_id(session.get()));
+                        cache::saveOlmSession(
+                          id_key, std::move(session), QDateTime::currentMSecsSinceEpoch());
+                    } 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());
+                    }
+                }
+                nhlog::net()->info("send_to_device: {}", user_id);
+            }
+
+            if (!messages.empty())
+                http::client()->send_to_device<mtx::events::msg::OlmEncrypted>(
+                  http::client()->generate_txn_id(), messages, [](mtx::http::RequestErr err) {
+                      if (err) {
+                          nhlog::net()->warn("failed to send "
+                                             "send_to_device "
+                                             "message: {}",
+                                             err->matrix_error.error);
+                      }
+                  });
+        };
+    };
+
+    if (!claims.one_time_keys.empty())
+        http::client()->claim_keys(claims, BindPks(pks));
+
+    if (!keysToQuery.empty()) {
+        mtx::requests::QueryKeys req;
+        req.device_keys = keysToQuery;
+        http::client()->query_keys(
+          req,
+          [ev_json, BindPks, our_curve](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));
+                  return;
+              }
+
+              nhlog::net()->info("queried keys");
+
+              cache::client()->updateUserKeys(cache::nextBatchToken(), res);
+
+              mtx::requests::ClaimKeys claim_keys;
+
+              std::map<std::string, std::map<std::string, DevicePublicKeys>> deviceKeys;
+
+              for (const auto &user : res.device_keys) {
+                  for (const auto &dev : user.second) {
+                      const auto user_id   = ::UserId(dev.second.user_id);
+                      const auto device_id = DeviceId(dev.second.device_id);
+
+                      if (user_id.get() == http::client()->user_id().to_string() &&
+                          device_id.get() == http::client()->device_id())
+                          continue;
+
+                      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);
+
+                      if (pks.curve25519 == our_curve) {
+                          nhlog::crypto()->warn("Skipping our own device, since sending "
+                                                "ourselves olm messages makes no sense.");
+                          continue;
+                      }
+
+                      try {
+                          if (!mtx::crypto::verify_identity_signature(
+                                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 currentTime = QDateTime::currentSecsSinceEpoch();
+                      if (rateLimit.value(QPair(user.first, device_id.get())) + 60 * 60 * 10 <
+                          currentTime) {
+                          deviceKeys[user_id].emplace(device_id, pks);
+                          claim_keys.one_time_keys[user.first][device_id] =
+                            mtx::crypto::SIGNED_CURVE25519;
+
+                          rateLimit.insert(QPair(user.first, device_id.get()), currentTime);
+                      } else {
+                          nhlog::crypto()->warn("Not creating new session with {}:{} "
+                                                "because of rate limit",
+                                                user.first,
+                                                device_id.get());
+                          continue;
+                      }
+
+                      nhlog::net()->info("{}", device_id.get());
+                      nhlog::net()->info("  curve25519 {}", pks.curve25519);
+                      nhlog::net()->info("  ed25519 {}", pks.ed25519);
+                  }
+              }
+
+              if (!claim_keys.one_time_keys.empty())
+                  http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
+          });
+    }
+}
+
+void
+request_cross_signing_keys()
+{
+    mtx::events::msg::SecretRequest secretRequest{};
+    secretRequest.action               = mtx::events::msg::RequestAction::Request;
+    secretRequest.requesting_device_id = http::client()->device_id();
+
+    auto local_user = http::client()->user_id();
+
+    auto verificationStatus = cache::verificationStatus(local_user.to_string());
+
+    if (!verificationStatus)
+        return;
+
+    auto request = [&](std::string secretName) {
+        secretRequest.name       = secretName;
+        secretRequest.request_id = "ss." + http::client()->generate_txn_id();
+
+        request_id_to_secret_name[secretRequest.request_id] = secretRequest.name;
+
+        std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::SecretRequest>>
+          body;
+
+        for (const auto &dev : verificationStatus->verified_devices) {
+            if (dev != secretRequest.requesting_device_id)
+                body[local_user][dev] = secretRequest;
+        }
+
+        http::client()->send_to_device<mtx::events::msg::SecretRequest>(
+          http::client()->generate_txn_id(),
+          body,
+          [request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::net()->error("Failed to send request for secrect '{}'", secretName);
+                  // Cancel request on UI thread
+                  QTimer::singleShot(1, cache::client(), [request_id]() {
+                      request_id_to_secret_name.erase(request_id);
+                  });
+                  return;
+              }
+          });
+
+        for (const auto &dev : verificationStatus->verified_devices) {
+            if (dev != secretRequest.requesting_device_id)
+                body[local_user][dev].action = mtx::events::msg::RequestAction::Cancellation;
+        }
+
+        // timeout after 15 min
+        QTimer::singleShot(15 * 60 * 1000, [secretRequest, body]() {
+            if (request_id_to_secret_name.count(secretRequest.request_id)) {
+                request_id_to_secret_name.erase(secretRequest.request_id);
+                http::client()->send_to_device<mtx::events::msg::SecretRequest>(
+                  http::client()->generate_txn_id(),
+                  body,
+                  [secretRequest](mtx::http::RequestErr err) {
+                      if (err) {
+                          nhlog::net()->error("Failed to cancel request for secrect '{}'",
+                                              secretRequest.name);
+                          return;
+                      }
+                  });
+            }
+        });
+    };
+
+    request(mtx::secret_storage::secrets::cross_signing_master);
+    request(mtx::secret_storage::secrets::cross_signing_self_signing);
+    request(mtx::secret_storage::secrets::cross_signing_user_signing);
+    request(mtx::secret_storage::secrets::megolm_backup_v1);
+}
+
+namespace {
+void
+unlock_secrets(const std::string &key,
+               const std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData> &secrets)
+{
+    http::client()->secret_storage_key(
+      key,
+      [secrets](mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->error("Failed to download secret storage key");
+              return;
+          }
+
+          emit ChatPage::instance()->downloadedSecrets(keyDesc, secrets);
+      });
+}
+}
+
+void
+download_cross_signing_keys()
+{
+    using namespace mtx::secret_storage;
+    http::client()->secret_storage_secret(
+      secrets::megolm_backup_v1, [](Secret secret, mtx::http::RequestErr err) {
+          std::optional<Secret> backup_key;
+          if (!err)
+              backup_key = secret;
+
+          http::client()->secret_storage_secret(
+            secrets::cross_signing_master, [backup_key](Secret secret, mtx::http::RequestErr err) {
+                std::optional<Secret> master_key;
+                if (!err)
+                    master_key = secret;
+
+                http::client()->secret_storage_secret(
+                  secrets::cross_signing_self_signing,
+                  [backup_key, master_key](Secret secret, mtx::http::RequestErr err) {
+                      std::optional<Secret> self_signing_key;
+                      if (!err)
+                          self_signing_key = secret;
+
+                      http::client()->secret_storage_secret(
+                        secrets::cross_signing_user_signing,
+                        [backup_key, self_signing_key, master_key](Secret secret,
+                                                                   mtx::http::RequestErr err) {
+                            std::optional<Secret> user_signing_key;
+                            if (!err)
+                                user_signing_key = secret;
+
+                            std::map<std::string, std::map<std::string, AesHmacSha2EncryptedData>>
+                              secrets;
+
+                            if (backup_key && !backup_key->encrypted.empty())
+                                secrets[backup_key->encrypted.begin()->first]
+                                       [secrets::megolm_backup_v1] =
+                                         backup_key->encrypted.begin()->second;
+
+                            if (master_key && !master_key->encrypted.empty())
+                                secrets[master_key->encrypted.begin()->first]
+                                       [secrets::cross_signing_master] =
+                                         master_key->encrypted.begin()->second;
+
+                            if (self_signing_key && !self_signing_key->encrypted.empty())
+                                secrets[self_signing_key->encrypted.begin()->first]
+                                       [secrets::cross_signing_self_signing] =
+                                         self_signing_key->encrypted.begin()->second;
+
+                            if (user_signing_key && !user_signing_key->encrypted.empty())
+                                secrets[user_signing_key->encrypted.begin()->first]
+                                       [secrets::cross_signing_user_signing] =
+                                         user_signing_key->encrypted.begin()->second;
+
+                            for (const auto &[key, secrets] : secrets)
+                                unlock_secrets(key, secrets);
+                        });
+                  });
+            });
+      });
+}
+
+} // namespace olm
diff --git a/src/Olm.h b/src/encryption/Olm.h
similarity index 76%
rename from src/Olm.h
rename to src/encryption/Olm.h
index ab86ca0062e5de908f9ebf9cfd179a11406d7457..44e2b8edb7a4237270214077a65a803ea5e69db9 100644
--- a/src/Olm.h
+++ b/src/encryption/Olm.h
@@ -18,32 +18,32 @@ Q_NAMESPACE
 
 enum DecryptionErrorCode
 {
-        NoError,
-        MissingSession, // Session was not found, retrieve from backup or request from other devices
-                        // and try again
-        MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
-                             // retrieve from backup or request from other devices and try again
-        DbError,             // DB read failed
-        DecryptionFailed,    // libolm error
-        ParsingFailed,       // Failed to parse the actual event
-        ReplayAttack,        // Megolm index reused
+    NoError,
+    MissingSession, // Session was not found, retrieve from backup or request from other devices
+                    // and try again
+    MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
+                         // retrieve from backup or request from other devices and try again
+    DbError,             // DB read failed
+    DecryptionFailed,    // libolm error
+    ParsingFailed,       // Failed to parse the actual event
+    ReplayAttack,        // Megolm index reused
 };
 Q_ENUM_NS(DecryptionErrorCode)
 
 struct DecryptionResult
 {
-        DecryptionErrorCode error;
-        std::optional<std::string> error_message;
-        std::optional<mtx::events::collections::TimelineEvents> event;
+    DecryptionErrorCode error;
+    std::optional<std::string> error_message;
+    std::optional<mtx::events::collections::TimelineEvents> event;
 };
 
 struct OlmMessage
 {
-        std::string sender_key;
-        std::string sender;
+    std::string sender_key;
+    std::string sender;
 
-        using RecipientKey = std::string;
-        std::map<RecipientKey, mtx::events::msg::OlmCipherContent> ciphertext;
+    using RecipientKey = std::string;
+    std::map<RecipientKey, mtx::events::msg::OlmCipherContent> ciphertext;
 };
 
 void
@@ -70,6 +70,8 @@ create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::R
 void
 import_inbound_megolm_session(
   const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey);
+void
+lookup_keybackup(const std::string room, const std::string session_id);
 
 nlohmann::json
 handle_pre_key_olm_message(const std::string &sender,
@@ -87,7 +89,7 @@ decryptEvent(const MegolmSessionIndex &index,
              const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event,
              bool dont_write_db = false);
 crypto::Trust
-calculate_trust(const std::string &user_id, const std::string &curve25519);
+calculate_trust(const std::string &user_id, const MegolmSessionIndex &index);
 
 void
 mark_keys_as_published();
diff --git a/src/encryption/SelfVerificationStatus.cpp b/src/encryption/SelfVerificationStatus.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ebb6b548843946d731589537c07f10a3e3339297
--- /dev/null
+++ b/src/encryption/SelfVerificationStatus.cpp
@@ -0,0 +1,312 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "SelfVerificationStatus.h"
+
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MainWindow.h"
+#include "MatrixClient.h"
+#include "Olm.h"
+#include "timeline/TimelineViewManager.h"
+#include "ui/UIA.h"
+
+#include <mtx/responses/common.hpp>
+
+SelfVerificationStatus::SelfVerificationStatus(QObject *o)
+  : QObject(o)
+{
+    connect(MainWindow::instance(), &MainWindow::reload, this, [this] {
+        connect(cache::client(),
+                &Cache::selfVerificationStatusChanged,
+                this,
+                &SelfVerificationStatus::invalidate,
+                Qt::UniqueConnection);
+        invalidate();
+    });
+}
+
+void
+SelfVerificationStatus::setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup)
+{
+    nhlog::db()->info("Clicked setup crossigning");
+
+    auto xsign_keys = olm::client()->create_crosssigning_keys();
+
+    if (!xsign_keys) {
+        nhlog::crypto()->critical("Failed to setup cross-signing keys!");
+        emit setupFailed(tr("Failed to create keys for cross-signing!"));
+        return;
+    }
+
+    cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_master,
+                                 xsign_keys->private_master_key);
+    cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_self_signing,
+                                 xsign_keys->private_self_signing_key);
+    cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_user_signing,
+                                 xsign_keys->private_user_signing_key);
+
+    std::optional<mtx::crypto::OlmClient::OnlineKeyBackupSetup> okb;
+    if (useOnlineKeyBackup) {
+        okb = olm::client()->create_online_key_backup(xsign_keys->private_master_key);
+        if (!okb) {
+            nhlog::crypto()->critical("Failed to setup online key backup!");
+            emit setupFailed(tr("Failed to create keys for online key backup!"));
+            return;
+        }
+
+        cache::client()->storeSecret(
+          mtx::secret_storage::secrets::megolm_backup_v1,
+          mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey)));
+
+        http::client()->post_backup_version(
+          okb->backupVersion.algorithm,
+          okb->backupVersion.auth_data,
+          [](const mtx::responses::Version &v, mtx::http::RequestErr e) {
+              if (e) {
+                  nhlog::net()->error("error setting up online key backup: {} {} {} {}",
+                                      e->parse_error,
+                                      e->status_code,
+                                      e->error_code,
+                                      e->matrix_error.error);
+              } else {
+                  nhlog::crypto()->info("Set up online key backup: '{}'", v.version);
+              }
+          });
+    }
+
+    std::optional<mtx::crypto::OlmClient::SSSSSetup> ssss;
+    if (useSSSS) {
+        ssss = olm::client()->create_ssss_key(password.toStdString());
+        if (!ssss) {
+            nhlog::crypto()->critical("Failed to setup secure server side secret storage!");
+            emit setupFailed(tr("Failed to create keys secure server side secret storage!"));
+            return;
+        }
+
+        auto master      = mtx::crypto::PkSigning::from_seed(xsign_keys->private_master_key);
+        nlohmann::json j = ssss->keyDescription;
+        j.erase("signatures");
+        ssss->keyDescription
+          .signatures[http::client()->user_id().to_string()]["ed25519:" + master.public_key()] =
+          master.sign(j.dump());
+
+        http::client()->upload_secret_storage_key(
+          ssss->keyDescription.name, ssss->keyDescription, [](mtx::http::RequestErr) {});
+        http::client()->set_secret_storage_default_key(ssss->keyDescription.name,
+                                                       [](mtx::http::RequestErr) {});
+
+        auto uploadSecret = [ssss](const std::string &key_name, const std::string &secret) {
+            mtx::secret_storage::Secret s;
+            s.encrypted[ssss->keyDescription.name] =
+              mtx::crypto::encrypt(secret, ssss->privateKey, key_name);
+            http::client()->upload_secret_storage_secret(
+              key_name, s, [key_name](mtx::http::RequestErr) {
+                  nhlog::crypto()->info("Uploaded secret: {}", key_name);
+              });
+        };
+
+        uploadSecret(mtx::secret_storage::secrets::cross_signing_master,
+                     xsign_keys->private_master_key);
+        uploadSecret(mtx::secret_storage::secrets::cross_signing_self_signing,
+                     xsign_keys->private_self_signing_key);
+        uploadSecret(mtx::secret_storage::secrets::cross_signing_user_signing,
+                     xsign_keys->private_user_signing_key);
+
+        if (okb)
+            uploadSecret(mtx::secret_storage::secrets::megolm_backup_v1,
+                         mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey)));
+    }
+
+    mtx::requests::DeviceSigningUpload device_sign{};
+    device_sign.master_key       = xsign_keys->master_key;
+    device_sign.self_signing_key = xsign_keys->self_signing_key;
+    device_sign.user_signing_key = xsign_keys->user_signing_key;
+    http::client()->device_signing_upload(
+      device_sign,
+      UIA::instance()->genericHandler(tr("Encryption Setup")),
+      [this, ssss, xsign_keys](mtx::http::RequestErr e) {
+          if (e) {
+              nhlog::crypto()->critical("Failed to upload cross signing keys: {}",
+                                        e->matrix_error.error);
+
+              emit setupFailed(tr("Encryption setup failed: %1")
+                                 .arg(QString::fromStdString(e->matrix_error.error)));
+              return;
+          }
+          nhlog::crypto()->info("Crosssigning keys uploaded!");
+
+          auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
+          if (deviceKeys) {
+              auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
+              if (myKey.user_id == http::client()->user_id().to_string() &&
+                  myKey.device_id == http::client()->device_id() &&
+                  myKey.keys["ed25519:" + http::client()->device_id()] ==
+                    olm::client()->identity_keys().ed25519 &&
+                  myKey.keys["curve25519:" + http::client()->device_id()] ==
+                    olm::client()->identity_keys().curve25519) {
+                  json j = myKey;
+                  j.erase("signatures");
+                  j.erase("unsigned");
+
+                  auto ssk =
+                    mtx::crypto::PkSigning::from_seed(xsign_keys->private_self_signing_key);
+                  myKey.signatures[http::client()->user_id().to_string()]
+                                  ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
+                  mtx::requests::KeySignaturesUpload req;
+                  req.signatures[http::client()->user_id().to_string()]
+                                [http::client()->device_id()] = myKey;
+
+                  http::client()->keys_signatures_upload(
+                    req,
+                    [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
+                        if (err) {
+                            nhlog::net()->error("failed to upload signatures: {},{}",
+                                                mtx::errors::to_string(err->matrix_error.errcode),
+                                                static_cast<int>(err->status_code));
+                        }
+
+                        for (const auto &[user_id, tmp] : res.errors)
+                            for (const auto &[key_id, e] : tmp)
+                                nhlog::net()->error("signature error for user {} and key "
+                                                    "id {}: {}, {}",
+                                                    user_id,
+                                                    key_id,
+                                                    mtx::errors::to_string(e.errcode),
+                                                    e.error);
+                    });
+              }
+          }
+
+          if (ssss) {
+              auto k = QString::fromStdString(mtx::crypto::key_to_recoverykey(ssss->privateKey));
+
+              QString r;
+              for (int i = 0; i < k.size(); i += 4)
+                  r += k.mid(i, 4) + " ";
+
+              emit showRecoveryKey(r.trimmed());
+          } else {
+              emit setupCompleted();
+          }
+      });
+}
+
+void
+SelfVerificationStatus::verifyMasterKey()
+{
+    nhlog::db()->info("Clicked verify master key");
+
+    const auto this_user = http::client()->user_id().to_string();
+
+    auto keys        = cache::client()->userKeys(this_user);
+    const auto &sigs = keys->master_keys.signatures[this_user];
+
+    std::vector<QString> devices;
+    for (const auto &[dev, sig] : sigs) {
+        (void)sig;
+
+        auto d = QString::fromStdString(dev);
+        if (d.startsWith("ed25519:")) {
+            d.remove("ed25519:");
+
+            if (keys->device_keys.count(d.toStdString()))
+                devices.push_back(std::move(d));
+        }
+    }
+
+    if (!devices.empty())
+        ChatPage::instance()->timelineManager()->verificationManager()->verifyOneOfDevices(
+          QString::fromStdString(this_user), std::move(devices));
+}
+
+void
+SelfVerificationStatus::verifyMasterKeyWithPassphrase()
+{
+    nhlog::db()->info("Clicked verify master key with passphrase");
+    olm::download_cross_signing_keys();
+}
+
+void
+SelfVerificationStatus::verifyUnverifiedDevices()
+{
+    nhlog::db()->info("Clicked verify unverified devices");
+    const auto this_user = http::client()->user_id().to_string();
+
+    auto keys  = cache::client()->userKeys(this_user);
+    auto verif = cache::client()->verificationStatus(this_user);
+
+    if (!keys)
+        return;
+
+    std::vector<QString> devices;
+    for (const auto &[device, keys] : keys->device_keys) {
+        (void)keys;
+        if (!verif.verified_devices.count(device))
+            devices.push_back(QString::fromStdString(device));
+    }
+
+    if (!devices.empty())
+        ChatPage::instance()->timelineManager()->verificationManager()->verifyOneOfDevices(
+          QString::fromStdString(this_user), std::move(devices));
+}
+
+void
+SelfVerificationStatus::invalidate()
+{
+    using namespace mtx::secret_storage;
+
+    nhlog::db()->info("Invalidating self verification status");
+    this->hasSSSS_ = false;
+    emit hasSSSSChanged();
+
+    auto keys = cache::client()->userKeys(http::client()->user_id().to_string());
+    if (!keys || keys->device_keys.find(http::client()->device_id()) == keys->device_keys.end()) {
+        cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()});
+        cache::client()->query_keys(http::client()->user_id().to_string(),
+                                    [](const UserKeyCache &, mtx::http::RequestErr) {});
+        return;
+    }
+
+    if (keys->master_keys.keys.empty()) {
+        if (status_ != SelfVerificationStatus::NoMasterKey) {
+            this->status_ = SelfVerificationStatus::NoMasterKey;
+            emit statusChanged();
+        }
+        return;
+    }
+
+    http::client()->secret_storage_secret(secrets::cross_signing_self_signing,
+                                          [this](Secret secret, mtx::http::RequestErr err) {
+                                              if (!err && !secret.encrypted.empty()) {
+                                                  this->hasSSSS_ = true;
+                                                  emit hasSSSSChanged();
+                                              }
+                                          });
+
+    auto verifStatus = cache::client()->verificationStatus(http::client()->user_id().to_string());
+
+    if (!verifStatus.user_verified) {
+        if (status_ != SelfVerificationStatus::UnverifiedMasterKey) {
+            this->status_ = SelfVerificationStatus::UnverifiedMasterKey;
+            emit statusChanged();
+        }
+        return;
+    }
+
+    if (verifStatus.unverified_device_count > 0) {
+        if (status_ != SelfVerificationStatus::UnverifiedDevices) {
+            this->status_ = SelfVerificationStatus::UnverifiedDevices;
+            emit statusChanged();
+        }
+        return;
+    }
+
+    if (status_ != SelfVerificationStatus::AllVerified) {
+        this->status_ = SelfVerificationStatus::AllVerified;
+        emit statusChanged();
+        return;
+    }
+}
diff --git a/src/encryption/SelfVerificationStatus.h b/src/encryption/SelfVerificationStatus.h
new file mode 100644
index 0000000000000000000000000000000000000000..b1f315f46594b25791d385e561ace3767f4b2eae
--- /dev/null
+++ b/src/encryption/SelfVerificationStatus.h
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+
+class SelfVerificationStatus : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+    Q_PROPERTY(bool hasSSSS READ hasSSSS NOTIFY hasSSSSChanged)
+
+public:
+    SelfVerificationStatus(QObject *o = nullptr);
+    enum Status
+    {
+        AllVerified,
+        NoMasterKey,
+        UnverifiedMasterKey,
+        UnverifiedDevices,
+    };
+    Q_ENUM(Status)
+
+    Q_INVOKABLE void setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup);
+    Q_INVOKABLE void verifyMasterKey();
+    Q_INVOKABLE void verifyMasterKeyWithPassphrase();
+    Q_INVOKABLE void verifyUnverifiedDevices();
+
+    Status status() const { return status_; }
+    bool hasSSSS() const { return hasSSSS_; }
+
+signals:
+    void statusChanged();
+    void hasSSSSChanged();
+    void setupCompleted();
+    void showRecoveryKey(QString key);
+    void setupFailed(QString message);
+
+public slots:
+    void invalidate();
+
+private:
+    Status status_ = AllVerified;
+    bool hasSSSS_  = true;
+};
diff --git a/src/encryption/VerificationManager.cpp b/src/encryption/VerificationManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f4c7ddf2dde8559c933c19f096ae1db6cad10567
--- /dev/null
+++ b/src/encryption/VerificationManager.cpp
@@ -0,0 +1,135 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "VerificationManager.h"
+#include "Cache.h"
+#include "ChatPage.h"
+#include "DeviceVerificationFlow.h"
+#include "timeline/TimelineViewManager.h"
+
+VerificationManager::VerificationManager(TimelineViewManager *o)
+  : QObject(o)
+  , rooms_(o->rooms())
+{}
+
+void
+VerificationManager::receivedRoomDeviceVerificationRequest(
+  const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
+  TimelineModel *model)
+{
+    if (this->isInitialSync_)
+        return;
+
+    auto event_id = QString::fromStdString(message.event_id);
+    if (!this->dvList.contains(event_id)) {
+        if (auto flow = DeviceVerificationFlow::NewInRoomVerification(
+              this, model, message.content, QString::fromStdString(message.sender), event_id)) {
+            dvList[event_id] = flow;
+            emit newDeviceVerificationRequest(flow.data());
+        }
+    }
+}
+
+void
+VerificationManager::receivedDeviceVerificationRequest(
+  const mtx::events::msg::KeyVerificationRequest &msg,
+  std::string sender)
+{
+    if (this->isInitialSync_)
+        return;
+
+    if (!msg.transaction_id)
+        return;
+
+    auto txnid = QString::fromStdString(msg.transaction_id.value());
+    if (!this->dvList.contains(txnid)) {
+        if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
+              this, msg, QString::fromStdString(sender), txnid)) {
+            dvList[txnid] = flow;
+            emit newDeviceVerificationRequest(flow.data());
+        }
+    }
+}
+
+void
+VerificationManager::receivedDeviceVerificationStart(
+  const mtx::events::msg::KeyVerificationStart &msg,
+  std::string sender)
+{
+    if (this->isInitialSync_)
+        return;
+
+    if (!msg.transaction_id)
+        return;
+
+    auto txnid = QString::fromStdString(msg.transaction_id.value());
+    if (!this->dvList.contains(txnid)) {
+        if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
+              this, msg, QString::fromStdString(sender), txnid)) {
+            dvList[txnid] = flow;
+            emit newDeviceVerificationRequest(flow.data());
+        }
+    }
+}
+
+void
+VerificationManager::verifyUser(QString userid)
+{
+    auto joined_rooms = cache::joinedRooms();
+    auto room_infos   = cache::getRoomInfo(joined_rooms);
+
+    for (std::string room_id : joined_rooms) {
+        if ((room_infos[QString::fromStdString(room_id)].member_count == 2) &&
+            cache::isRoomEncrypted(room_id)) {
+            auto room_members = cache::roomMembers(room_id);
+            if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) !=
+                room_members.end()) {
+                if (auto model = rooms_->getRoomById(QString::fromStdString(room_id))) {
+                    auto flow =
+                      DeviceVerificationFlow::InitiateUserVerification(this, model.data(), userid);
+                    connect(model.data(),
+                            &TimelineModel::updateFlowEventId,
+                            this,
+                            [this, flow](std::string eventId) {
+                                dvList[QString::fromStdString(eventId)] = flow;
+                            });
+                    emit newDeviceVerificationRequest(flow.data());
+                    return;
+                }
+            }
+        }
+    }
+
+    emit ChatPage::instance()->showNotification(
+      tr("No encrypted private chat found with this user. Create an "
+         "encrypted private chat with this user and try again."));
+}
+
+void
+VerificationManager::removeVerificationFlow(DeviceVerificationFlow *flow)
+{
+    for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) {
+        if ((*it).second == flow) {
+            dvList.remove((*it).first);
+            return;
+        }
+    }
+}
+
+void
+VerificationManager::verifyDevice(QString userid, QString deviceid)
+{
+    auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, {deviceid});
+    this->dvList[flow->transactionId()] = flow;
+    emit newDeviceVerificationRequest(flow.data());
+}
+
+void
+VerificationManager::verifyOneOfDevices(QString userid, std::vector<QString> deviceids)
+{
+    auto flow =
+      DeviceVerificationFlow::InitiateDeviceVerification(this, userid, std::move(deviceids));
+    this->dvList[flow->transactionId()] = flow;
+    emit newDeviceVerificationRequest(flow.data());
+}
diff --git a/src/encryption/VerificationManager.h b/src/encryption/VerificationManager.h
new file mode 100644
index 0000000000000000000000000000000000000000..da646c2f8613a04fdd5e019659709a6414094777
--- /dev/null
+++ b/src/encryption/VerificationManager.h
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QHash>
+#include <QObject>
+#include <QSharedPointer>
+
+#include <mtx/events.hpp>
+#include <mtx/events/encrypted.hpp>
+
+class DeviceVerificationFlow;
+class TimelineModel;
+class TimelineModel;
+class TimelineViewManager;
+class RoomlistModel;
+
+class VerificationManager : public QObject
+{
+    Q_OBJECT
+
+public:
+    VerificationManager(TimelineViewManager *o = nullptr);
+
+    Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
+    void verifyUser(QString userid);
+    void verifyDevice(QString userid, QString deviceid);
+    void verifyOneOfDevices(QString userid, std::vector<QString> deviceids);
+
+signals:
+    void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
+
+public slots:
+    void receivedRoomDeviceVerificationRequest(
+      const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
+      TimelineModel *model);
+    void receivedDeviceVerificationRequest(const mtx::events::msg::KeyVerificationRequest &msg,
+                                           std::string sender);
+    void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &msg,
+                                         std::string sender);
+
+private:
+    QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;
+    bool isInitialSync_ = false;
+    RoomlistModel *rooms_;
+};
diff --git a/src/main.cpp b/src/main.cpp
index 29e93d49374e3b80eb6feb526fbc02ddfe04270f..f6373d2ac9c44b03177cc74ab5ffe8fe8b6b27e2 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -18,7 +18,6 @@
 #include <QMessageBox>
 #include <QPoint>
 #include <QScreen>
-#include <QSettings>
 #include <QStandardPaths>
 #include <QTranslator>
 
@@ -49,47 +48,47 @@ QQmlDebuggingEnabler enabler;
 void
 stacktraceHandler(int signum)
 {
-        std::signal(signum, SIG_DFL);
+    std::signal(signum, SIG_DFL);
 
-        // boost::stacktrace::safe_dump_to("./nheko-backtrace.dump");
+    // boost::stacktrace::safe_dump_to("./nheko-backtrace.dump");
 
-        // see
-        // https://stackoverflow.com/questions/77005/how-to-automatically-generate-a-stacktrace-when-my-program-crashes/77336#77336
-        void *array[50];
-        size_t size;
+    // see
+    // https://stackoverflow.com/questions/77005/how-to-automatically-generate-a-stacktrace-when-my-program-crashes/77336#77336
+    void *array[50];
+    size_t size;
 
-        // get void*'s for all entries on the stack
-        size = backtrace(array, 50);
+    // get void*'s for all entries on the stack
+    size = backtrace(array, 50);
 
-        // print out all the frames to stderr
-        fprintf(stderr, "Error: signal %d:\n", signum);
-        backtrace_symbols_fd(array, size, STDERR_FILENO);
+    // print out all the frames to stderr
+    fprintf(stderr, "Error: signal %d:\n", signum);
+    backtrace_symbols_fd(array, size, STDERR_FILENO);
 
-        int file = ::open("/tmp/nheko-crash.dump",
-                          O_CREAT | O_WRONLY | O_TRUNC
+    int file = ::open("/tmp/nheko-crash.dump",
+                      O_CREAT | O_WRONLY | O_TRUNC
 #if defined(S_IWUSR) && defined(S_IRUSR)
-                          ,
-                          S_IWUSR | S_IRUSR
+                      ,
+                      S_IWUSR | S_IRUSR
 #elif defined(S_IWRITE) && defined(S_IREAD)
-                          ,
-                          S_IWRITE | S_IREAD
+                      ,
+                      S_IWRITE | S_IREAD
 #endif
-        );
-        if (file != -1) {
-                constexpr char header[]   = "Error: signal\n";
-                [[maybe_unused]] auto ret = write(file, header, std::size(header) - 1);
-                backtrace_symbols_fd(array, size, file);
-                close(file);
-        }
-
-        std::raise(SIGABRT);
+    );
+    if (file != -1) {
+        constexpr char header[]   = "Error: signal\n";
+        [[maybe_unused]] auto ret = write(file, header, std::size(header) - 1);
+        backtrace_symbols_fd(array, size, file);
+        close(file);
+    }
+
+    std::raise(SIGABRT);
 }
 
 void
 registerSignalHandlers()
 {
-        std::signal(SIGSEGV, &stacktraceHandler);
-        std::signal(SIGABRT, &stacktraceHandler);
+    std::signal(SIGSEGV, &stacktraceHandler);
+    std::signal(SIGABRT, &stacktraceHandler);
 }
 
 #else
@@ -104,203 +103,203 @@ registerSignalHandlers()
 QPoint
 screenCenter(int width, int height)
 {
-        // Deprecated in 5.13: QRect screenGeometry = QApplication::desktop()->screenGeometry();
-        QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
+    // Deprecated in 5.13: QRect screenGeometry = QApplication::desktop()->screenGeometry();
+    QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
 
-        int x = (screenGeometry.width() - width) / 2;
-        int y = (screenGeometry.height() - height) / 2;
+    int x = (screenGeometry.width() - width) / 2;
+    int y = (screenGeometry.height() - height) / 2;
 
-        return QPoint(x, y);
+    return QPoint(x, y);
 }
 
 void
 createStandardDirectory(QStandardPaths::StandardLocation path)
 {
-        auto dir = QStandardPaths::writableLocation(path);
+    auto dir = QStandardPaths::writableLocation(path);
 
-        if (!QDir().mkpath(dir)) {
-                throw std::runtime_error(
-                  ("Unable to create state directory:" + dir).toStdString().c_str());
-        }
+    if (!QDir().mkpath(dir)) {
+        throw std::runtime_error(("Unable to create state directory:" + dir).toStdString().c_str());
+    }
 }
 
 int
 main(int argc, char *argv[])
 {
-        QCoreApplication::setApplicationName("nheko");
-        QCoreApplication::setApplicationVersion(nheko::version);
-        QCoreApplication::setOrganizationName("nheko");
-        QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
-        QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
-        QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
-
-        // this needs to be after setting the application name. Or how would we find our settings
-        // file then?
+    QCoreApplication::setApplicationName("nheko");
+    QCoreApplication::setApplicationVersion(nheko::version);
+    QCoreApplication::setOrganizationName("nheko");
+    QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
+    QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
+    // this needs to be after setting the application name. Or how would we find our settings
+    // file then?
 #if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD)
-        if (qgetenv("QT_SCALE_FACTOR").size() == 0) {
-                float factor = utils::scaleFactor();
+    if (qgetenv("QT_SCALE_FACTOR").size() == 0) {
+        float factor = utils::scaleFactor();
 
-                if (factor != -1)
-                        qputenv("QT_SCALE_FACTOR", QString::number(factor).toUtf8());
-        }
+        if (factor != -1)
+            qputenv("QT_SCALE_FACTOR", QString::number(factor).toUtf8());
+    }
 #endif
 
-        // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name
-        // parsed before the SingleApplication userdata is set.
-        QString userdata{""};
-        QString matrixUri;
-        for (int i = 1; i < argc; ++i) {
-                QString arg{argv[i]};
-                if (arg.startsWith("--profile=")) {
-                        arg.remove("--profile=");
-                        userdata = arg;
-                } else if (arg.startsWith("--p=")) {
-                        arg.remove("-p=");
-                        userdata = arg;
-                } else if (arg == "--profile" || arg == "-p") {
-                        if (i < argc - 1) // if i is less than argc - 1, we still have a parameter
-                                          // left to process as the name
-                        {
-                                ++i; // the next arg is the name, so increment
-                                userdata = QString{argv[i]};
-                        }
-                } else if (arg.startsWith("matrix:")) {
-                        matrixUri = arg;
-                }
-        }
-
-        SingleApplication app(argc,
-                              argv,
-                              true,
-                              SingleApplication::Mode::User |
-                                SingleApplication::Mode::ExcludeAppPath |
-                                SingleApplication::Mode::ExcludeAppVersion |
-                                SingleApplication::Mode::SecondaryNotification,
-                              100,
-                              userdata);
-
-        if (app.isSecondary()) {
-                // open uri in main instance
-                app.sendMessage(matrixUri.toUtf8());
-                return 0;
-        }
-
-        QCommandLineParser parser;
-        parser.addHelpOption();
-        parser.addVersionOption();
-        QCommandLineOption debugOption("debug", "Enable debug output");
-        parser.addOption(debugOption);
-
-        // This option is not actually parsed via Qt due to the need to parse it before the app
-        // name is set. It only exists to keep Qt from complaining about the --profile/-p
-        // option and thereby crashing the app.
-        QCommandLineOption configName(
-          QStringList() << "p"
-                        << "profile",
-          QCoreApplication::tr("Create a unique profile, which allows you to log into several "
-                               "accounts at the same time and start multiple instances of nheko."),
-          QCoreApplication::tr("profile"),
-          QCoreApplication::tr("profile name"));
-        parser.addOption(configName);
-
-        parser.process(app);
-
-        app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"}));
-
-        http::init();
-
-        createStandardDirectory(QStandardPaths::CacheLocation);
-        createStandardDirectory(QStandardPaths::AppDataLocation);
-
-        registerSignalHandlers();
-
-        if (parser.isSet(debugOption))
-                nhlog::enable_debug_log_from_commandline = true;
-
-        try {
-                nhlog::init(QString("%1/nheko.log")
-                              .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
-                              .toStdString());
-        } catch (const spdlog::spdlog_ex &ex) {
-                std::cout << "Log initialization failed: " << ex.what() << std::endl;
-                std::exit(1);
-        }
-
-        if (parser.isSet(configName))
-                UserSettings::initialize(parser.value(configName));
-        else
-                UserSettings::initialize(std::nullopt);
-
-        auto settings = UserSettings::instance().toWeakRef();
-
-        QFont font;
-        QString userFontFamily = settings.lock()->font();
-        if (!userFontFamily.isEmpty() && userFontFamily != "default") {
-                font.setFamily(userFontFamily);
+    // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name
+    // parsed before the SingleApplication userdata is set.
+    QString userdata{""};
+    QString matrixUri;
+    for (int i = 1; i < argc; ++i) {
+        QString arg{argv[i]};
+        if (arg.startsWith("--profile=")) {
+            arg.remove("--profile=");
+            userdata = arg;
+        } else if (arg.startsWith("--p=")) {
+            arg.remove("-p=");
+            userdata = arg;
+        } else if (arg == "--profile" || arg == "-p") {
+            if (i < argc - 1) // if i is less than argc - 1, we still have a parameter
+                              // left to process as the name
+            {
+                ++i; // the next arg is the name, so increment
+                userdata = QString{argv[i]};
+            }
+        } else if (arg.startsWith("matrix:")) {
+            matrixUri = arg;
         }
-        font.setPointSizeF(settings.lock()->fontSize());
-
-        app.setFont(font);
-
-        QString lang = QLocale::system().name();
-
-        QTranslator qtTranslator;
-        qtTranslator.load(
-          QLocale(), "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath));
-        app.installTranslator(&qtTranslator);
-
-        QTranslator appTranslator;
-        appTranslator.load(QLocale(), "nheko", "_", ":/translations");
-        app.installTranslator(&appTranslator);
-
-        MainWindow w;
-
-        // Move the MainWindow to the center
-        w.move(screenCenter(w.width(), w.height()));
-
-        if (!(settings.lock()->startInTray() && settings.lock()->tray()))
-                w.show();
-
-        QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() {
-                w.saveCurrentWindowSize();
-                if (http::client() != nullptr) {
-                        nhlog::net()->debug("shutting down all I/O threads & open connections");
-                        http::client()->close(true);
-                        nhlog::net()->debug("bye");
-                }
-        });
-        QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() {
-                w.show();
-                w.raise();
-                w.activateWindow();
-        });
-
-        QObject::connect(
-          &app,
-          &SingleApplication::receivedMessage,
-          ChatPage::instance(),
-          [&](quint32, QByteArray message) { ChatPage::instance()->handleMatrixUri(message); });
-
-        QMetaObject::Connection uriConnection;
-        if (app.isPrimary() && !matrixUri.isEmpty()) {
-                uriConnection = QObject::connect(ChatPage::instance(),
-                                                 &ChatPage::contentLoaded,
-                                                 ChatPage::instance(),
-                                                 [&uriConnection, matrixUri]() {
-                                                         ChatPage::instance()->handleMatrixUri(
-                                                           matrixUri.toUtf8());
-                                                         QObject::disconnect(uriConnection);
-                                                 });
+    }
+
+    SingleApplication app(argc,
+                          argv,
+                          true,
+                          SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppPath |
+                            SingleApplication::Mode::ExcludeAppVersion |
+                            SingleApplication::Mode::SecondaryNotification,
+                          100,
+                          userdata);
+
+    QCommandLineParser parser;
+    parser.addHelpOption();
+    parser.addVersionOption();
+    QCommandLineOption debugOption("debug", "Enable debug output");
+    parser.addOption(debugOption);
+
+    // This option is not actually parsed via Qt due to the need to parse it before the app
+    // name is set. It only exists to keep Qt from complaining about the --profile/-p
+    // option and thereby crashing the app.
+    QCommandLineOption configName(
+      QStringList() << "p"
+                    << "profile",
+      QCoreApplication::tr("Create a unique profile, which allows you to log into several "
+                           "accounts at the same time and start multiple instances of nheko."),
+      QCoreApplication::tr("profile"),
+      QCoreApplication::tr("profile name"));
+    parser.addOption(configName);
+
+    parser.process(app);
+
+    // This check needs to happen _after_ process(), so that we actually print help for --help when
+    // Nheko is already running.
+    if (app.isSecondary()) {
+        nhlog::ui()->info("Sending Matrix URL to main application: {}", matrixUri.toStdString());
+        // open uri in main instance
+        app.sendMessage(matrixUri.toUtf8());
+        return 0;
+    }
+
+    app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"}));
+
+    http::init();
+
+    createStandardDirectory(QStandardPaths::CacheLocation);
+    createStandardDirectory(QStandardPaths::AppDataLocation);
+
+    registerSignalHandlers();
+
+    if (parser.isSet(debugOption))
+        nhlog::enable_debug_log_from_commandline = true;
+
+    try {
+        nhlog::init(QString("%1/nheko.log")
+                      .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                      .toStdString());
+    } catch (const spdlog::spdlog_ex &ex) {
+        std::cout << "Log initialization failed: " << ex.what() << std::endl;
+        std::exit(1);
+    }
+
+    if (parser.isSet(configName))
+        UserSettings::initialize(parser.value(configName));
+    else
+        UserSettings::initialize(std::nullopt);
+
+    auto settings = UserSettings::instance().toWeakRef();
+
+    QFont font;
+    QString userFontFamily = settings.lock()->font();
+    if (!userFontFamily.isEmpty() && userFontFamily != "default") {
+        font.setFamily(userFontFamily);
+    }
+    font.setPointSizeF(settings.lock()->fontSize());
+
+    app.setFont(font);
+
+    QString lang = QLocale::system().name();
+
+    QTranslator qtTranslator;
+    qtTranslator.load(QLocale(), "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+    app.installTranslator(&qtTranslator);
+
+    QTranslator appTranslator;
+    appTranslator.load(QLocale(), "nheko", "_", ":/translations");
+    app.installTranslator(&appTranslator);
+
+    MainWindow w;
+
+    // Move the MainWindow to the center
+    w.move(screenCenter(w.width(), w.height()));
+
+    if (!(settings.lock()->startInTray() && settings.lock()->tray()))
+        w.show();
+
+    QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() {
+        w.saveCurrentWindowSize();
+        if (http::client() != nullptr) {
+            nhlog::net()->debug("shutting down all I/O threads & open connections");
+            http::client()->close(true);
+            nhlog::net()->debug("bye");
         }
-        QDesktopServices::setUrlHandler("matrix", ChatPage::instance(), "handleMatrixUri");
+    });
+    QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() {
+        w.show();
+        w.raise();
+        w.activateWindow();
+    });
+
+    QObject::connect(
+      &app,
+      &SingleApplication::receivedMessage,
+      ChatPage::instance(),
+      [&](quint32, QByteArray message) { ChatPage::instance()->handleMatrixUri(message); });
+
+    QMetaObject::Connection uriConnection;
+    if (app.isPrimary() && !matrixUri.isEmpty()) {
+        uriConnection =
+          QObject::connect(ChatPage::instance(),
+                           &ChatPage::contentLoaded,
+                           ChatPage::instance(),
+                           [&uriConnection, matrixUri]() {
+                               ChatPage::instance()->handleMatrixUri(matrixUri.toUtf8());
+                               QObject::disconnect(uriConnection);
+                           });
+    }
+    QDesktopServices::setUrlHandler("matrix", ChatPage::instance(), "handleMatrixUri");
 
 #if defined(Q_OS_MAC)
-        // Temporary solution for the emoji picker until
-        // nheko has a proper menu bar with more functionality.
-        MacHelper::initializeMenus();
+    // Temporary solution for the emoji picker until
+    // nheko has a proper menu bar with more functionality.
+    MacHelper::initializeMenus();
 #endif
 
-        nhlog::ui()->info("starting nheko {}", nheko::version);
+    nhlog::ui()->info("starting nheko {}", nheko::version);
 
-        return app.exec();
+    return app.exec();
 }
diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp
index be580b08fb225b4a01cbbe1bb9f389431105a225..5d51c6c8624fecf21b8e814f6658d17c36532cb8 100644
--- a/src/notifications/Manager.cpp
+++ b/src/notifications/Manager.cpp
@@ -11,30 +11,24 @@
 QString
 NotificationsManager::getMessageTemplate(const mtx::responses::Notification &notification)
 {
-        const auto sender =
-          cache::displayName(QString::fromStdString(notification.room_id),
-                             QString::fromStdString(mtx::accessors::sender(notification.event)));
+    const auto sender =
+      cache::displayName(QString::fromStdString(notification.room_id),
+                         QString::fromStdString(mtx::accessors::sender(notification.event)));
 
-        // TODO: decrypt this message if the decryption setting is on in the UserSettings
-        if (auto msg = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-              &notification.event);
-            msg != nullptr) {
-                return tr("%1 sent an encrypted message").arg(sender);
-        }
+    // TODO: decrypt this message if the decryption setting is on in the UserSettings
+    if (auto msg = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+          &notification.event);
+        msg != nullptr) {
+        return tr("%1 sent an encrypted message").arg(sender);
+    }
 
-        if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) {
-                return tr("* %1 %2",
-                          "Format an emote message in a notification, %1 is the sender, %2 the "
-                          "message")
-                  .arg(sender);
-        } else if (utils::isReply(notification.event)) {
-                return tr("%1 replied: %2",
-                          "Format a reply in a notification. %1 is the sender, %2 the message")
-                  .arg(sender);
-        } else {
-                return tr("%1: %2",
-                          "Format a normal message in a notification. %1 is the sender, %2 the "
-                          "message")
-                  .arg(sender);
-        }
+    if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) {
+        return QString("* %1 %2").arg(sender);
+    } else if (utils::isReply(notification.event)) {
+        return tr("%1 replied: %2",
+                  "Format a reply in a notification. %1 is the sender, %2 the message")
+          .arg(sender);
+    } else {
+        return QString("%1: %2").arg(sender);
+    }
 }
diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index 416530e0e32cd2fc2edbde2acd07b9da9114c07e..da9302964d9d5596d6a49e332434c0e7ac284e12 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -22,83 +22,85 @@
 
 struct roomEventId
 {
-        QString roomId;
-        QString eventId;
+    QString roomId;
+    QString eventId;
 };
 
 inline bool
 operator==(const roomEventId &a, const roomEventId &b)
 {
-        return a.roomId == b.roomId && a.eventId == b.eventId;
+    return a.roomId == b.roomId && a.eventId == b.eventId;
 }
 
 class NotificationsManager : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        NotificationsManager(QObject *parent = nullptr);
+    NotificationsManager(QObject *parent = nullptr);
 
-        void postNotification(const mtx::responses::Notification &notification, const QImage &icon);
+    void postNotification(const mtx::responses::Notification &notification, const QImage &icon);
 
 signals:
-        void notificationClicked(const QString roomId, const QString eventId);
-        void sendNotificationReply(const QString roomId, const QString eventId, const QString body);
-        void systemPostNotificationCb(const QString &room_id,
-                                      const QString &event_id,
-                                      const QString &roomName,
-                                      const QString &text,
-                                      const QImage &icon);
+    void notificationClicked(const QString roomId, const QString eventId);
+    void sendNotificationReply(const QString roomId, const QString eventId, const QString body);
+    void systemPostNotificationCb(const QString &room_id,
+                                  const QString &event_id,
+                                  const QString &roomName,
+                                  const QString &text,
+                                  const QImage &icon);
 
 public slots:
-        void removeNotification(const QString &roomId, const QString &eventId);
+    void removeNotification(const QString &roomId, const QString &eventId);
 
 #if defined(NHEKO_DBUS_SYS)
 public:
-        void closeNotifications(QString roomId);
+    void closeNotifications(QString roomId);
 
 private:
-        QDBusInterface dbus;
+    QDBusInterface dbus;
 
-        void systemPostNotification(const QString &room_id,
-                                    const QString &event_id,
-                                    const QString &roomName,
-                                    const QString &text,
-                                    const QImage &icon);
-        void closeNotification(uint id);
+    void systemPostNotification(const QString &room_id,
+                                const QString &event_id,
+                                const QString &roomName,
+                                const QString &text,
+                                const QImage &icon);
+    void closeNotification(uint id);
 
-        // notification ID to (room ID, event ID)
-        QMap<uint, roomEventId> notificationIds;
+    // notification ID to (room ID, event ID)
+    QMap<uint, roomEventId> notificationIds;
 
-        const bool hasMarkup_;
-        const bool hasImages_;
+    const bool hasMarkup_;
+    const bool hasImages_;
 #endif
 
 #if defined(Q_OS_MACOS)
 private:
-        // Objective-C(++) doesn't like to do lots of regular C++, so the actual notification
-        // posting is split out
-        void objCxxPostNotification(const QString &title,
-                                    const QString &subtitle,
-                                    const QString &informativeText,
-                                    const QImage &bodyImage);
+    // Objective-C(++) doesn't like to do lots of regular C++, so the actual notification
+    // posting is split out
+    void objCxxPostNotification(const QString &room_name,
+                                const QString &room_id,
+                                const QString &event_id,
+                                const QString &subtitle,
+                                const QString &informativeText,
+                                const QString &bodyImagePath);
 #endif
 
 #if defined(Q_OS_WINDOWS)
 private:
-        void systemPostNotification(const QString &line1,
-                                    const QString &line2,
-                                    const QString &iconPath);
+    void systemPostNotification(const QString &line1,
+                                const QString &line2,
+                                const QString &iconPath);
 #endif
 
-        // these slots are platform specific (D-Bus only)
-        // but Qt slot declarations can not be inside an ifdef!
+    // these slots are platform specific (D-Bus only)
+    // but Qt slot declarations can not be inside an ifdef!
 private slots:
-        void actionInvoked(uint id, QString action);
-        void notificationClosed(uint id, uint reason);
-        void notificationReplied(uint id, QString reply);
+    void actionInvoked(uint id, QString action);
+    void notificationClosed(uint id, uint reason);
+    void notificationReplied(uint id, QString reply);
 
 private:
-        QString getMessageTemplate(const mtx::responses::Notification &notification);
+    QString getMessageTemplate(const mtx::responses::Notification &notification);
 };
 
 #if defined(NHEKO_DBUS_SYS)
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index 2809de87ac6e0ebade8502e400e149d0c3e4cb52..758cb615397ad85ad04f2d1dd788f94d8d69bdd6 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -34,109 +34,100 @@ NotificationsManager::NotificationsManager(QObject *parent)
          QDBusConnection::sessionBus(),
          this)
   , hasMarkup_{std::invoke([this]() -> bool {
-          for (auto x : dbus.call("GetCapabilities").arguments())
-                  if (x.toStringList().contains("body-markup"))
-                          return true;
-          return false;
+      for (auto x : dbus.call("GetCapabilities").arguments())
+          if (x.toStringList().contains("body-markup"))
+              return true;
+      return false;
   })}
   , hasImages_{std::invoke([this]() -> bool {
-          for (auto x : dbus.call("GetCapabilities").arguments())
-                  if (x.toStringList().contains("body-images"))
-                          return true;
-          return false;
+      for (auto x : dbus.call("GetCapabilities").arguments())
+          if (x.toStringList().contains("body-images"))
+              return true;
+      return false;
   })}
 {
-        qDBusRegisterMetaType<QImage>();
+    qDBusRegisterMetaType<QImage>();
 
-        QDBusConnection::sessionBus().connect("org.freedesktop.Notifications",
-                                              "/org/freedesktop/Notifications",
-                                              "org.freedesktop.Notifications",
-                                              "ActionInvoked",
-                                              this,
-                                              SLOT(actionInvoked(uint, QString)));
-        QDBusConnection::sessionBus().connect("org.freedesktop.Notifications",
-                                              "/org/freedesktop/Notifications",
-                                              "org.freedesktop.Notifications",
-                                              "NotificationClosed",
-                                              this,
-                                              SLOT(notificationClosed(uint, uint)));
-        QDBusConnection::sessionBus().connect("org.freedesktop.Notifications",
-                                              "/org/freedesktop/Notifications",
-                                              "org.freedesktop.Notifications",
-                                              "NotificationReplied",
-                                              this,
-                                              SLOT(notificationReplied(uint, QString)));
+    QDBusConnection::sessionBus().connect("org.freedesktop.Notifications",
+                                          "/org/freedesktop/Notifications",
+                                          "org.freedesktop.Notifications",
+                                          "ActionInvoked",
+                                          this,
+                                          SLOT(actionInvoked(uint, QString)));
+    QDBusConnection::sessionBus().connect("org.freedesktop.Notifications",
+                                          "/org/freedesktop/Notifications",
+                                          "org.freedesktop.Notifications",
+                                          "NotificationClosed",
+                                          this,
+                                          SLOT(notificationClosed(uint, uint)));
+    QDBusConnection::sessionBus().connect("org.freedesktop.Notifications",
+                                          "/org/freedesktop/Notifications",
+                                          "org.freedesktop.Notifications",
+                                          "NotificationReplied",
+                                          this,
+                                          SLOT(notificationReplied(uint, QString)));
 
-        connect(this,
-                &NotificationsManager::systemPostNotificationCb,
-                this,
-                &NotificationsManager::systemPostNotification,
-                Qt::QueuedConnection);
+    connect(this,
+            &NotificationsManager::systemPostNotificationCb,
+            this,
+            &NotificationsManager::systemPostNotification,
+            Qt::QueuedConnection);
 }
 
 void
 NotificationsManager::postNotification(const mtx::responses::Notification &notification,
                                        const QImage &icon)
 {
-        const auto room_id  = QString::fromStdString(notification.room_id);
-        const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
-        const auto room_name =
-          QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
+    const auto room_id   = QString::fromStdString(notification.room_id);
+    const auto event_id  = QString::fromStdString(mtx::accessors::event_id(notification.event));
+    const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
 
-        auto postNotif = [this, room_id, event_id, room_name, icon](QString text) {
-                emit systemPostNotificationCb(room_id, event_id, room_name, text, icon);
-        };
-
-        QString template_ = getMessageTemplate(notification);
-        // TODO: decrypt this message if the decryption setting is on in the UserSettings
-        if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-              notification.event)) {
-                postNotif(template_);
-                return;
-        }
+    auto postNotif = [this, room_id, event_id, room_name, icon](QString text) {
+        emit systemPostNotificationCb(room_id, event_id, room_name, text, icon);
+    };
 
-        if (hasMarkup_) {
-                if (hasImages_ && mtx::accessors::msg_type(notification.event) ==
-                                    mtx::events::MessageType::Image) {
-                        MxcImageProvider::download(
-                          QString::fromStdString(mtx::accessors::url(notification.event))
-                            .remove("mxc://"),
-                          QSize(200, 80),
-                          [postNotif, notification, template_](
-                            QString, QSize, QImage, QString imgPath) {
-                                  if (imgPath.isEmpty())
-                                          postNotif(template_
-                                                      .arg(utils::stripReplyFallbacks(
-                                                             notification.event, {}, {})
-                                                             .quoted_formatted_body)
-                                                      .replace("<em>", "<i>")
-                                                      .replace("</em>", "</i>")
-                                                      .replace("<strong>", "<b>")
-                                                      .replace("</strong>", "</b>"));
-                                  else
-                                          postNotif(template_.arg(
-                                            QStringLiteral("<br><img src=\"file:///") % imgPath %
-                                            "\" alt=\"" %
-                                            mtx::accessors::formattedBodyWithFallback(
-                                              notification.event) %
-                                            "\">"));
-                          });
-                        return;
-                }
+    QString template_ = getMessageTemplate(notification);
+    // TODO: decrypt this message if the decryption setting is on in the UserSettings
+    if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+          notification.event)) {
+        postNotif(template_);
+        return;
+    }
 
-                postNotif(
-                  template_
-                    .arg(
-                      utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body)
-                    .replace("<em>", "<i>")
-                    .replace("</em>", "</i>")
-                    .replace("<strong>", "<b>")
-                    .replace("</strong>", "</b>"));
-                return;
+    if (hasMarkup_) {
+        if (hasImages_ &&
+            mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) {
+            MxcImageProvider::download(
+              QString::fromStdString(mtx::accessors::url(notification.event)).remove("mxc://"),
+              QSize(200, 80),
+              [postNotif, notification, template_](QString, QSize, QImage, QString imgPath) {
+                  if (imgPath.isEmpty())
+                      postNotif(template_
+                                  .arg(utils::stripReplyFallbacks(notification.event, {}, {})
+                                         .quoted_formatted_body)
+                                  .replace("<em>", "<i>")
+                                  .replace("</em>", "</i>")
+                                  .replace("<strong>", "<b>")
+                                  .replace("</strong>", "</b>"));
+                  else
+                      postNotif(template_.arg(
+                        QStringLiteral("<br><img src=\"file:///") % imgPath % "\" alt=\"" %
+                        mtx::accessors::formattedBodyWithFallback(notification.event) % "\">"));
+              });
+            return;
         }
 
         postNotif(
-          template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body));
+          template_
+            .arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body)
+            .replace("<em>", "<i>")
+            .replace("</em>", "</i>")
+            .replace("<strong>", "<b>")
+            .replace("</strong>", "</b>"));
+        return;
+    }
+
+    postNotif(template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body));
 }
 
 /**
@@ -152,99 +143,99 @@ NotificationsManager::systemPostNotification(const QString &room_id,
                                              const QString &text,
                                              const QImage &icon)
 {
-        QVariantMap hints;
-        hints["image-data"] = icon;
-        hints["sound-name"] = "message-new-instant";
-        QList<QVariant> argumentList;
-        argumentList << "nheko";  // app_name
-        argumentList << (uint)0;  // replace_id
-        argumentList << "";       // app_icon
-        argumentList << roomName; // summary
-        argumentList << text;     // body
+    QVariantMap hints;
+    hints["image-data"] = icon;
+    hints["sound-name"] = "message-new-instant";
+    QList<QVariant> argumentList;
+    argumentList << "nheko";  // app_name
+    argumentList << (uint)0;  // replace_id
+    argumentList << "";       // app_icon
+    argumentList << roomName; // summary
+    argumentList << text;     // body
 
-        // The list of actions has always the action name and then a localized version of that
-        // action. Currently we just use an empty string for that.
-        // TODO(Nico): Look into what to actually put there.
-        argumentList << (QStringList("default") << ""
-                                                << "inline-reply"
-                                                << ""); // actions
-        argumentList << hints;                          // hints
-        argumentList << (int)-1;                        // timeout in ms
+    // The list of actions has always the action name and then a localized version of that
+    // action. Currently we just use an empty string for that.
+    // TODO(Nico): Look into what to actually put there.
+    argumentList << (QStringList("default") << ""
+                                            << "inline-reply"
+                                            << ""); // actions
+    argumentList << hints;                          // hints
+    argumentList << (int)-1;                        // timeout in ms
 
-        QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList);
-        auto watcher          = new QDBusPendingCallWatcher{call, this};
-        connect(
-          watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() {
-                  if (watcher->reply().type() == QDBusMessage::ErrorMessage)
-                          qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
-                  else
-                          notificationIds[watcher->reply().arguments().first().toUInt()] =
-                            roomEventId{room_id, event_id};
-                  watcher->deleteLater();
-          });
+    QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList);
+    auto watcher          = new QDBusPendingCallWatcher{call, this};
+    connect(
+      watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() {
+          if (watcher->reply().type() == QDBusMessage::ErrorMessage)
+              qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
+          else
+              notificationIds[watcher->reply().arguments().first().toUInt()] =
+                roomEventId{room_id, event_id};
+          watcher->deleteLater();
+      });
 }
 
 void
 NotificationsManager::closeNotification(uint id)
 {
-        auto call    = dbus.asyncCall("CloseNotification", (uint)id); // replace_id
-        auto watcher = new QDBusPendingCallWatcher{call, this};
-        connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
-                if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
-                        qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
-                };
-                watcher->deleteLater();
-        });
+    auto call    = dbus.asyncCall("CloseNotification", (uint)id); // replace_id
+    auto watcher = new QDBusPendingCallWatcher{call, this};
+    connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
+        if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
+            qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
+        };
+        watcher->deleteLater();
+    });
 }
 
 void
 NotificationsManager::removeNotification(const QString &roomId, const QString &eventId)
 {
-        roomEventId reId = {roomId, eventId};
-        for (auto elem = notificationIds.begin(); elem != notificationIds.end(); ++elem) {
-                if (elem.value().roomId != roomId)
-                        continue;
+    roomEventId reId = {roomId, eventId};
+    for (auto elem = notificationIds.begin(); elem != notificationIds.end(); ++elem) {
+        if (elem.value().roomId != roomId)
+            continue;
 
-                // close all notifications matching the eventId or having a lower
-                // notificationId
-                // This relies on the notificationId not wrapping around. This allows for
-                // approximately 2,147,483,647 notifications, so it is a bit unlikely.
-                // Otherwise we would need to store a 64bit counter instead.
-                closeNotification(elem.key());
+        // close all notifications matching the eventId or having a lower
+        // notificationId
+        // This relies on the notificationId not wrapping around. This allows for
+        // approximately 2,147,483,647 notifications, so it is a bit unlikely.
+        // Otherwise we would need to store a 64bit counter instead.
+        closeNotification(elem.key());
 
-                // FIXME: compare index of event id of the read receipt and the notification instead
-                // of just the id to prevent read receipts of events without notification clearing
-                // all notifications in that room!
-                if (elem.value() == reId)
-                        break;
-        }
+        // FIXME: compare index of event id of the read receipt and the notification instead
+        // of just the id to prevent read receipts of events without notification clearing
+        // all notifications in that room!
+        if (elem.value() == reId)
+            break;
+    }
 }
 
 void
 NotificationsManager::actionInvoked(uint id, QString action)
 {
-        if (notificationIds.contains(id)) {
-                roomEventId idEntry = notificationIds[id];
-                if (action == "default") {
-                        emit notificationClicked(idEntry.roomId, idEntry.eventId);
-                }
+    if (notificationIds.contains(id)) {
+        roomEventId idEntry = notificationIds[id];
+        if (action == "default") {
+            emit notificationClicked(idEntry.roomId, idEntry.eventId);
         }
+    }
 }
 
 void
 NotificationsManager::notificationReplied(uint id, QString reply)
 {
-        if (notificationIds.contains(id)) {
-                roomEventId idEntry = notificationIds[id];
-                emit sendNotificationReply(idEntry.roomId, idEntry.eventId, reply);
-        }
+    if (notificationIds.contains(id)) {
+        roomEventId idEntry = notificationIds[id];
+        emit sendNotificationReply(idEntry.roomId, idEntry.eventId, reply);
+    }
 }
 
 void
 NotificationsManager::notificationClosed(uint id, uint reason)
 {
-        Q_UNUSED(reason);
-        notificationIds.remove(id);
+    Q_UNUSED(reason);
+    notificationIds.remove(id);
 }
 
 /**
@@ -259,52 +250,52 @@ NotificationsManager::notificationClosed(uint id, uint reason)
 QDBusArgument &
 operator<<(QDBusArgument &arg, const QImage &image)
 {
-        if (image.isNull()) {
-                arg.beginStructure();
-                arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray();
-                arg.endStructure();
-                return arg;
-        }
+    if (image.isNull()) {
+        arg.beginStructure();
+        arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray();
+        arg.endStructure();
+        return arg;
+    }
 
-        QImage scaled = image.scaledToHeight(100, Qt::SmoothTransformation);
-        scaled        = scaled.convertToFormat(QImage::Format_ARGB32);
+    QImage scaled = image.scaledToHeight(100, Qt::SmoothTransformation);
+    scaled        = scaled.convertToFormat(QImage::Format_ARGB32);
 
 #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
-        // ABGR -> ARGB
-        QImage i = scaled.rgbSwapped();
+    // ABGR -> ARGB
+    QImage i = scaled.rgbSwapped();
 #else
-        // ABGR -> GBAR
-        QImage i(scaled.size(), scaled.format());
-        for (int y = 0; y < i.height(); ++y) {
-                QRgb *p   = (QRgb *)scaled.scanLine(y);
-                QRgb *q   = (QRgb *)i.scanLine(y);
-                QRgb *end = p + scaled.width();
-                while (p < end) {
-                        *q = qRgba(qGreen(*p), qBlue(*p), qAlpha(*p), qRed(*p));
-                        p++;
-                        q++;
-                }
+    // ABGR -> GBAR
+    QImage i(scaled.size(), scaled.format());
+    for (int y = 0; y < i.height(); ++y) {
+        QRgb *p   = (QRgb *)scaled.scanLine(y);
+        QRgb *q   = (QRgb *)i.scanLine(y);
+        QRgb *end = p + scaled.width();
+        while (p < end) {
+            *q = qRgba(qGreen(*p), qBlue(*p), qAlpha(*p), qRed(*p));
+            p++;
+            q++;
         }
+    }
 #endif
 
-        arg.beginStructure();
-        arg << i.width();
-        arg << i.height();
-        arg << i.bytesPerLine();
-        arg << i.hasAlphaChannel();
-        int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3);
-        arg << i.depth() / channels;
-        arg << channels;
-        arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes());
-        arg.endStructure();
+    arg.beginStructure();
+    arg << i.width();
+    arg << i.height();
+    arg << i.bytesPerLine();
+    arg << i.hasAlphaChannel();
+    int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3);
+    arg << i.depth() / channels;
+    arg << channels;
+    arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes());
+    arg.endStructure();
 
-        return arg;
+    return arg;
 }
 
 const QDBusArgument &
 operator>>(const QDBusArgument &arg, QImage &)
 {
-        // This is needed to link but shouldn't be called.
-        Q_ASSERT(0);
-        return arg;
+    // This is needed to link but shouldn't be called.
+    Q_ASSERT(0);
+    return arg;
 }
diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp
index 8e36985cd77d6959ad0c249df09a36650ec2e3e0..30948daeb12f7a1aed3899212a879ce86095a12a 100644
--- a/src/notifications/ManagerMac.cpp
+++ b/src/notifications/ManagerMac.cpp
@@ -19,48 +19,50 @@
 static QString
 formatNotification(const mtx::responses::Notification &notification)
 {
-        return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body;
+    return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body;
 }
 
 void
 NotificationsManager::postNotification(const mtx::responses::Notification &notification,
                                        const QImage &icon)
 {
-        Q_UNUSED(icon)
+    Q_UNUSED(icon)
 
-        const auto room_name =
-          QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
-        const auto sender =
-          cache::displayName(QString::fromStdString(notification.room_id),
-                             QString::fromStdString(mtx::accessors::sender(notification.event)));
+    const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
+    const auto sender =
+      cache::displayName(QString::fromStdString(notification.room_id),
+                         QString::fromStdString(mtx::accessors::sender(notification.event)));
 
-        const auto isEncrypted =
-          std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-            &notification.event) != nullptr;
-        const auto isReply = utils::isReply(notification.event);
-        if (isEncrypted) {
-                // TODO: decrypt this message if the decryption setting is on in the UserSettings
-                const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message")
-                                                     : tr("%1 sent an encrypted message"))
-                                              .arg(sender);
-                objCxxPostNotification(room_name, messageInfo, "", QImage());
-        } else {
-                const QString messageInfo =
-                  (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender);
-                if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image)
-                        MxcImageProvider::download(
-                          QString::fromStdString(mtx::accessors::url(notification.event))
-                            .remove("mxc://"),
-                          QSize(200, 80),
-                          [this, notification, room_name, messageInfo](
-                            QString, QSize, QImage image, QString) {
-                                  objCxxPostNotification(room_name,
-                                                         messageInfo,
-                                                         formatNotification(notification),
-                                                         image);
-                          });
-                else
-                        objCxxPostNotification(
-                          room_name, messageInfo, formatNotification(notification), QImage());
-        }
+    const auto room_id  = QString::fromStdString(notification.room_id);
+    const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
+
+    const auto isEncrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+                               &notification.event) != nullptr;
+    const auto isReply = utils::isReply(notification.event);
+    if (isEncrypted) {
+        // TODO: decrypt this message if the decryption setting is on in the UserSettings
+        const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message")
+                                             : tr("%1 sent an encrypted message"))
+                                      .arg(sender);
+        objCxxPostNotification(room_name, room_id, event_id, messageInfo, "", "");
+    } else {
+        const QString messageInfo =
+          (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender);
+        if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image)
+            MxcImageProvider::download(
+              QString::fromStdString(mtx::accessors::url(notification.event)).remove("mxc://"),
+              QSize(200, 80),
+              [this, notification, room_name, room_id, event_id, messageInfo](
+                QString, QSize, QImage, QString imgPath) {
+                  objCxxPostNotification(room_name,
+                                         room_id,
+                                         event_id,
+                                         messageInfo,
+                                         formatNotification(notification),
+                                         imgPath);
+              });
+        else
+            objCxxPostNotification(
+              room_name, room_id, event_id, messageInfo, formatNotification(notification), "");
+    }
 }
diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm
index 33b7b6aff146a4327346114aef1dfc829a3d307e..8669432b6b40f05c2b3bc0bdc99065078479d965 100644
--- a/src/notifications/ManagerMac.mm
+++ b/src/notifications/ManagerMac.mm
@@ -2,12 +2,36 @@
 
 #import <Foundation/Foundation.h>
 #import <AppKit/NSImage.h>
+#import <UserNotifications/UserNotifications.h>
 
 #include <QtMac>
 #include <QImage>
 
-@interface NSUserNotification (CFIPrivate)
-- (void)set_identityImage:(NSImage *)image;
+@interface UNNotificationAttachment (UNNotificationAttachmentAdditions)
+    + (UNNotificationAttachment *) createFromImageData:(NSData*)imgData identifier:(NSString *)imageFileIdentifier options:(NSDictionary*)attachmentOptions;
+@end
+
+@implementation UNNotificationAttachment (UNNotificationAttachmentAdditions)
+    + (UNNotificationAttachment *) createFromImageData:(NSData*)imgData identifier:(NSString *)imageFileIdentifier options:(NSDictionary*)attachmentOptions {
+        NSFileManager *fileManager = [NSFileManager defaultManager];
+        NSString *tmpSubFolderName = [[NSProcessInfo processInfo] globallyUniqueString];
+        NSURL *tmpSubFolderURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:tmpSubFolderName] isDirectory:true];
+        NSError *error = nil;
+        [fileManager createDirectoryAtURL:tmpSubFolderURL withIntermediateDirectories:true attributes:nil error:&error];
+        if(error) {
+            NSLog(@"%@",[error localizedDescription]);
+            return nil;
+        }
+        NSURL *fileURL = [tmpSubFolderURL URLByAppendingPathComponent:imageFileIdentifier];
+        [imgData writeToURL:fileURL atomically:true];
+        UNNotificationAttachment *imageAttachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:fileURL options:attachmentOptions error:&error];
+        if(error) {
+            NSLog(@"%@",[error localizedDescription]);
+            return nil;
+        }
+        return imageAttachment;
+
+    }
 @end
 
 NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
@@ -16,24 +40,54 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
 }
 
 void
-NotificationsManager::objCxxPostNotification(const QString &title,
+NotificationsManager::objCxxPostNotification(const QString &room_name,
+                                             const QString &room_id,
+                                             const QString &event_id,
                                              const QString &subtitle,
                                              const QString &informativeText,
-                                             const QImage &bodyImage)
+                                             const QString &bodyImagePath)
 {
+    UNAuthorizationOptions options = UNAuthorizationOptionAlert + UNAuthorizationOptionSound;
+    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+
+    [center requestAuthorizationWithOptions:options
+    completionHandler:^(BOOL granted, NSError * _Nullable error) {
+        if (!granted) {
+            NSLog(@"No notification access");
+            if (error) {
+                NSLog(@"%@",[error localizedDescription]);
+            }
+        }
+    }];
+
+    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
+
+    content.title             = room_name.toNSString();
+    content.subtitle          = subtitle.toNSString();
+    content.body              = informativeText.toNSString();
+    content.sound             = [UNNotificationSound defaultSound];
+    content.threadIdentifier  = room_id.toNSString();
 
-    NSUserNotification *notif = [[NSUserNotification alloc] init];
+    if (!bodyImagePath.isEmpty()) {
+        NSURL *imageURL = [NSURL fileURLWithPath:bodyImagePath.toNSString()];
+        NSData *img = [NSData dataWithContentsOfURL:imageURL];
+        NSArray *attachments = [NSMutableArray array];
+        UNNotificationAttachment *attachment = [UNNotificationAttachment createFromImageData:img identifier:@"attachment_image.jpeg" options:nil];
+        if (attachment) {
+            attachments = [NSMutableArray arrayWithObjects: attachment, nil];
+            content.attachments = attachments;
+        }
+    }
 
-    notif.title           = title.toNSString();
-    notif.subtitle        = subtitle.toNSString();
-    notif.informativeText = informativeText.toNSString();
-    notif.soundName       = NSUserNotificationDefaultSoundName;
+    UNNotificationRequest *notificationRequest = [UNNotificationRequest requestWithIdentifier:event_id.toNSString() content:content trigger:nil];
 
-    if (!bodyImage.isNull())
-        notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage.toCGImage() size: NSZeroSize];
+    [center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) {
+        if (error != nil) {
+            NSLog(@"Unable to Add Notification Request");
+        }
+    }];
 
-    [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif];
-    [notif autorelease];
+    [content autorelease];
 }
 
 //unused
diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index fe7830a76f9b46b7440ea9a5949420a8364371b8..4376e4d81ea811cea2bdce8cbb972241940c7ef2 100644
--- a/src/notifications/ManagerWin.cpp
+++ b/src/notifications/ManagerWin.cpp
@@ -20,10 +20,10 @@ using namespace WinToastLib;
 class CustomHandler : public IWinToastHandler
 {
 public:
-        void toastActivated() const {}
-        void toastActivated(int) const {}
-        void toastFailed() const { std::wcout << L"Error showing current toast" << std::endl; }
-        void toastDismissed(WinToastDismissalReason) const {}
+    void toastActivated() const {}
+    void toastActivated(int) const {}
+    void toastFailed() const { std::wcout << L"Error showing current toast" << std::endl; }
+    void toastDismissed(WinToastDismissalReason) const {}
 };
 
 namespace {
@@ -32,12 +32,12 @@ bool isInitialized = false;
 void
 init()
 {
-        isInitialized = true;
+    isInitialized = true;
 
-        WinToast::instance()->setAppName(L"Nheko");
-        WinToast::instance()->setAppUserModelId(WinToast::configureAUMI(L"nheko", L"nheko"));
-        if (!WinToast::instance()->initialize())
-                std::wcout << "Your system is not compatible with toast notifications\n";
+    WinToast::instance()->setAppName(L"Nheko");
+    WinToast::instance()->setAppUserModelId(WinToast::configureAUMI(L"nheko", L"nheko"));
+    if (!WinToast::instance()->initialize())
+        std::wcout << "Your system is not compatible with toast notifications\n";
 }
 }
 
@@ -49,41 +49,37 @@ void
 NotificationsManager::postNotification(const mtx::responses::Notification &notification,
                                        const QImage &icon)
 {
-        const auto room_name =
-          QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
-        const auto sender =
-          cache::displayName(QString::fromStdString(notification.room_id),
-                             QString::fromStdString(mtx::accessors::sender(notification.event)));
-
-        const auto isEncrypted =
-          std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-            &notification.event) != nullptr;
-        const auto isReply = utils::isReply(notification.event);
-
-        auto formatNotification = [this, notification, sender] {
-                const auto template_ = getMessageTemplate(notification);
-                if (std::holds_alternative<
-                      mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                      notification.event)) {
-                        return template_;
-                }
-
-                return template_.arg(
-                  utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body);
-        };
-
-        const auto line1 =
-          (room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name);
-        const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message")
-                                                   : tr("%1 sent an encrypted message"))
-                                        : formatNotification());
-
-        auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
-                        room_name + "-room-avatar.png";
-        if (!icon.save(iconPath))
-                iconPath.clear();
-
-        systemPostNotification(line1, line2, iconPath);
+    const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
+    const auto sender =
+      cache::displayName(QString::fromStdString(notification.room_id),
+                         QString::fromStdString(mtx::accessors::sender(notification.event)));
+
+    const auto isEncrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+                               &notification.event) != nullptr;
+    const auto isReply = utils::isReply(notification.event);
+
+    auto formatNotification = [this, notification, sender] {
+        const auto template_ = getMessageTemplate(notification);
+        if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+              notification.event)) {
+            return template_;
+        }
+
+        return template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body);
+    };
+
+    const auto line1 =
+      (room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name);
+    const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message")
+                                               : tr("%1 sent an encrypted message"))
+                                    : formatNotification());
+
+    auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + room_name +
+                    "-room-avatar.png";
+    if (!icon.save(iconPath))
+        iconPath.clear();
+
+    systemPostNotification(line1, line2, iconPath);
 }
 
 void
@@ -91,17 +87,17 @@ NotificationsManager::systemPostNotification(const QString &line1,
                                              const QString &line2,
                                              const QString &iconPath)
 {
-        if (!isInitialized)
-                init();
+    if (!isInitialized)
+        init();
 
-        auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
-        templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine);
-        templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine);
+    auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
+    templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine);
+    templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine);
 
-        if (!iconPath.isNull())
-                templ.setImagePath(iconPath.toStdWString());
+    if (!iconPath.isNull())
+        templ.setImagePath(iconPath.toStdWString());
 
-        WinToast::instance()->showToast(templ, new CustomHandler());
+    WinToast::instance()->showToast(templ, new CustomHandler());
 }
 
 void NotificationsManager::actionInvoked(uint, QString) {}
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 97bfa76d58dfdfd53af7c8b45b22d1163b2c5834..77bed3871a17f1b9e9b5f4d107745e2849841d6f 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -16,231 +16,230 @@ CommunitiesModel::CommunitiesModel(QObject *parent)
 QHash<int, QByteArray>
 CommunitiesModel::roleNames() const
 {
-        return {
-          {AvatarUrl, "avatarUrl"},
-          {DisplayName, "displayName"},
-          {Tooltip, "tooltip"},
-          {ChildrenHidden, "childrenHidden"},
-          {Hidden, "hidden"},
-          {Id, "id"},
-        };
+    return {
+      {AvatarUrl, "avatarUrl"},
+      {DisplayName, "displayName"},
+      {Tooltip, "tooltip"},
+      {ChildrenHidden, "childrenHidden"},
+      {Hidden, "hidden"},
+      {Id, "id"},
+    };
 }
 
 QVariant
 CommunitiesModel::data(const QModelIndex &index, int role) const
 {
-        if (index.row() == 0) {
-                switch (role) {
-                case CommunitiesModel::Roles::AvatarUrl:
-                        return QString(":/icons/icons/ui/world.png");
-                case CommunitiesModel::Roles::DisplayName:
-                        return tr("All rooms");
-                case CommunitiesModel::Roles::Tooltip:
-                        return tr("Shows all rooms without filtering.");
-                case CommunitiesModel::Roles::ChildrenHidden:
-                        return false;
-                case CommunitiesModel::Roles::Hidden:
-                        return false;
-                case CommunitiesModel::Roles::Id:
-                        return "";
-                }
-        } else if (index.row() - 1 < spaceOrder_.size()) {
-                auto id = spaceOrder_.at(index.row() - 1);
-                switch (role) {
-                case CommunitiesModel::Roles::AvatarUrl:
-                        return QString::fromStdString(spaces_.at(id).avatar_url);
-                case CommunitiesModel::Roles::DisplayName:
-                case CommunitiesModel::Roles::Tooltip:
-                        return QString::fromStdString(spaces_.at(id).name);
-                case CommunitiesModel::Roles::ChildrenHidden:
-                        return true;
-                case CommunitiesModel::Roles::Hidden:
-                        return hiddentTagIds_.contains("space:" + id);
-                case CommunitiesModel::Roles::Id:
-                        return "space:" + id;
-                }
-        } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) {
-                auto tag = tags_.at(index.row() - 1 - spaceOrder_.size());
-                if (tag == "m.favourite") {
-                        switch (role) {
-                        case CommunitiesModel::Roles::AvatarUrl:
-                                return QString(":/icons/icons/ui/star.png");
-                        case CommunitiesModel::Roles::DisplayName:
-                                return tr("Favourites");
-                        case CommunitiesModel::Roles::Tooltip:
-                                return tr("Rooms you have favourited.");
-                        }
-                } else if (tag == "m.lowpriority") {
-                        switch (role) {
-                        case CommunitiesModel::Roles::AvatarUrl:
-                                return QString(":/icons/icons/ui/lowprio.png");
-                        case CommunitiesModel::Roles::DisplayName:
-                                return tr("Low Priority");
-                        case CommunitiesModel::Roles::Tooltip:
-                                return tr("Rooms with low priority.");
-                        }
-                } else if (tag == "m.server_notice") {
-                        switch (role) {
-                        case CommunitiesModel::Roles::AvatarUrl:
-                                return QString(":/icons/icons/ui/tag.png");
-                        case CommunitiesModel::Roles::DisplayName:
-                                return tr("Server Notices");
-                        case CommunitiesModel::Roles::Tooltip:
-                                return tr("Messages from your server or administrator.");
-                        }
-                } else {
-                        switch (role) {
-                        case CommunitiesModel::Roles::AvatarUrl:
-                                return QString(":/icons/icons/ui/tag.png");
-                        case CommunitiesModel::Roles::DisplayName:
-                        case CommunitiesModel::Roles::Tooltip:
-                                return tag.mid(2);
-                        }
-                }
+    if (index.row() == 0) {
+        switch (role) {
+        case CommunitiesModel::Roles::AvatarUrl:
+            return QString(":/icons/icons/ui/world.png");
+        case CommunitiesModel::Roles::DisplayName:
+            return tr("All rooms");
+        case CommunitiesModel::Roles::Tooltip:
+            return tr("Shows all rooms without filtering.");
+        case CommunitiesModel::Roles::ChildrenHidden:
+            return false;
+        case CommunitiesModel::Roles::Hidden:
+            return false;
+        case CommunitiesModel::Roles::Id:
+            return "";
+        }
+    } else if (index.row() - 1 < spaceOrder_.size()) {
+        auto id = spaceOrder_.at(index.row() - 1);
+        switch (role) {
+        case CommunitiesModel::Roles::AvatarUrl:
+            return QString::fromStdString(spaces_.at(id).avatar_url);
+        case CommunitiesModel::Roles::DisplayName:
+        case CommunitiesModel::Roles::Tooltip:
+            return QString::fromStdString(spaces_.at(id).name);
+        case CommunitiesModel::Roles::ChildrenHidden:
+            return true;
+        case CommunitiesModel::Roles::Hidden:
+            return hiddentTagIds_.contains("space:" + id);
+        case CommunitiesModel::Roles::Id:
+            return "space:" + id;
+        }
+    } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) {
+        auto tag = tags_.at(index.row() - 1 - spaceOrder_.size());
+        if (tag == "m.favourite") {
+            switch (role) {
+            case CommunitiesModel::Roles::AvatarUrl:
+                return QString(":/icons/icons/ui/star.png");
+            case CommunitiesModel::Roles::DisplayName:
+                return tr("Favourites");
+            case CommunitiesModel::Roles::Tooltip:
+                return tr("Rooms you have favourited.");
+            }
+        } else if (tag == "m.lowpriority") {
+            switch (role) {
+            case CommunitiesModel::Roles::AvatarUrl:
+                return QString(":/icons/icons/ui/lowprio.png");
+            case CommunitiesModel::Roles::DisplayName:
+                return tr("Low Priority");
+            case CommunitiesModel::Roles::Tooltip:
+                return tr("Rooms with low priority.");
+            }
+        } else if (tag == "m.server_notice") {
+            switch (role) {
+            case CommunitiesModel::Roles::AvatarUrl:
+                return QString(":/icons/icons/ui/tag.png");
+            case CommunitiesModel::Roles::DisplayName:
+                return tr("Server Notices");
+            case CommunitiesModel::Roles::Tooltip:
+                return tr("Messages from your server or administrator.");
+            }
+        } else {
+            switch (role) {
+            case CommunitiesModel::Roles::AvatarUrl:
+                return QString(":/icons/icons/ui/tag.png");
+            case CommunitiesModel::Roles::DisplayName:
+            case CommunitiesModel::Roles::Tooltip:
+                return tag.mid(2);
+            }
+        }
 
-                switch (role) {
-                case CommunitiesModel::Roles::Hidden:
-                        return hiddentTagIds_.contains("tag:" + tag);
-                case CommunitiesModel::Roles::ChildrenHidden:
-                        return true;
-                case CommunitiesModel::Roles::Id:
-                        return "tag:" + tag;
-                }
+        switch (role) {
+        case CommunitiesModel::Roles::Hidden:
+            return hiddentTagIds_.contains("tag:" + tag);
+        case CommunitiesModel::Roles::ChildrenHidden:
+            return true;
+        case CommunitiesModel::Roles::Id:
+            return "tag:" + tag;
         }
-        return QVariant();
+    }
+    return QVariant();
 }
 
 void
 CommunitiesModel::initializeSidebar()
 {
-        beginResetModel();
-        tags_.clear();
-        spaceOrder_.clear();
-        spaces_.clear();
-
-        std::set<std::string> ts;
-        std::vector<RoomInfo> tempSpaces;
-        auto infos = cache::roomInfo();
-        for (auto it = infos.begin(); it != infos.end(); it++) {
-                if (it.value().is_space) {
-                        spaceOrder_.push_back(it.key());
-                        spaces_[it.key()] = it.value();
-                } else {
-                        for (const auto &t : it.value().tags) {
-                                if (t.find("u.") == 0 || t.find("m." == 0)) {
-                                        ts.insert(t);
-                                }
-                        }
+    beginResetModel();
+    tags_.clear();
+    spaceOrder_.clear();
+    spaces_.clear();
+
+    std::set<std::string> ts;
+    std::vector<RoomInfo> tempSpaces;
+    auto infos = cache::roomInfo();
+    for (auto it = infos.begin(); it != infos.end(); it++) {
+        if (it.value().is_space) {
+            spaceOrder_.push_back(it.key());
+            spaces_[it.key()] = it.value();
+        } else {
+            for (const auto &t : it.value().tags) {
+                if (t.find("u.") == 0 || t.find("m." == 0)) {
+                    ts.insert(t);
                 }
+            }
         }
+    }
 
-        for (const auto &t : ts)
-                tags_.push_back(QString::fromStdString(t));
+    for (const auto &t : ts)
+        tags_.push_back(QString::fromStdString(t));
 
-        hiddentTagIds_ = UserSettings::instance()->hiddenTags();
-        endResetModel();
+    hiddentTagIds_ = UserSettings::instance()->hiddenTags();
+    endResetModel();
 
-        emit tagsChanged();
-        emit hiddenTagsChanged();
+    emit tagsChanged();
+    emit hiddenTagsChanged();
 }
 
 void
 CommunitiesModel::clear()
 {
-        beginResetModel();
-        tags_.clear();
-        endResetModel();
-        resetCurrentTagId();
+    beginResetModel();
+    tags_.clear();
+    endResetModel();
+    resetCurrentTagId();
 
-        emit tagsChanged();
+    emit tagsChanged();
 }
 
 void
 CommunitiesModel::sync(const mtx::responses::Rooms &rooms)
 {
-        bool tagsUpdated = false;
-
-        for (const auto &[roomid, room] : rooms.join) {
-                (void)roomid;
-                for (const auto &e : room.account_data.events)
-                        if (std::holds_alternative<
-                              mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) {
-                                tagsUpdated = true;
-                        }
-                for (const auto &e : room.state.events)
-                        if (std::holds_alternative<
-                              mtx::events::StateEvent<mtx::events::state::space::Child>>(e) ||
-                            std::holds_alternative<
-                              mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) {
-                                tagsUpdated = true;
-                        }
-                for (const auto &e : room.timeline.events)
-                        if (std::holds_alternative<
-                              mtx::events::StateEvent<mtx::events::state::space::Child>>(e) ||
-                            std::holds_alternative<
-                              mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) {
-                                tagsUpdated = true;
-                        }
-        }
-        for (const auto &[roomid, room] : rooms.leave) {
-                (void)room;
-                if (spaceOrder_.contains(QString::fromStdString(roomid)))
-                        tagsUpdated = true;
-        }
-
-        if (tagsUpdated)
-                initializeSidebar();
+    bool tagsUpdated = false;
+
+    for (const auto &[roomid, room] : rooms.join) {
+        (void)roomid;
+        for (const auto &e : room.account_data.events)
+            if (std::holds_alternative<
+                  mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) {
+                tagsUpdated = true;
+            }
+        for (const auto &e : room.state.events)
+            if (std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Child>>(
+                  e) ||
+                std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Parent>>(
+                  e)) {
+                tagsUpdated = true;
+            }
+        for (const auto &e : room.timeline.events)
+            if (std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Child>>(
+                  e) ||
+                std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Parent>>(
+                  e)) {
+                tagsUpdated = true;
+            }
+    }
+    for (const auto &[roomid, room] : rooms.leave) {
+        (void)room;
+        if (spaceOrder_.contains(QString::fromStdString(roomid)))
+            tagsUpdated = true;
+    }
+
+    if (tagsUpdated)
+        initializeSidebar();
 }
 
 void
 CommunitiesModel::setCurrentTagId(QString tagId)
 {
-        if (tagId.startsWith("tag:")) {
-                auto tag = tagId.mid(4);
-                for (const auto &t : tags_) {
-                        if (t == tag) {
-                                this->currentTagId_ = tagId;
-                                emit currentTagIdChanged(currentTagId_);
-                                return;
-                        }
-                }
-        } else if (tagId.startsWith("space:")) {
-                auto tag = tagId.mid(6);
-                for (const auto &t : spaceOrder_) {
-                        if (t == tag) {
-                                this->currentTagId_ = tagId;
-                                emit currentTagIdChanged(currentTagId_);
-                                return;
-                        }
-                }
+    if (tagId.startsWith("tag:")) {
+        auto tag = tagId.mid(4);
+        for (const auto &t : tags_) {
+            if (t == tag) {
+                this->currentTagId_ = tagId;
+                emit currentTagIdChanged(currentTagId_);
+                return;
+            }
+        }
+    } else if (tagId.startsWith("space:")) {
+        auto tag = tagId.mid(6);
+        for (const auto &t : spaceOrder_) {
+            if (t == tag) {
+                this->currentTagId_ = tagId;
+                emit currentTagIdChanged(currentTagId_);
+                return;
+            }
         }
+    }
 
-        this->currentTagId_ = "";
-        emit currentTagIdChanged(currentTagId_);
+    this->currentTagId_ = "";
+    emit currentTagIdChanged(currentTagId_);
 }
 
 void
 CommunitiesModel::toggleTagId(QString tagId)
 {
-        if (hiddentTagIds_.contains(tagId)) {
-                hiddentTagIds_.removeOne(tagId);
-                UserSettings::instance()->setHiddenTags(hiddentTagIds_);
-        } else {
-                hiddentTagIds_.push_back(tagId);
-                UserSettings::instance()->setHiddenTags(hiddentTagIds_);
-        }
-
-        if (tagId.startsWith("tag:")) {
-                auto idx = tags_.indexOf(tagId.mid(4));
-                if (idx != -1)
-                        emit dataChanged(index(idx + 1 + spaceOrder_.size()),
-                                         index(idx + 1 + spaceOrder_.size()),
-                                         {Hidden});
-        } else if (tagId.startsWith("space:")) {
-                auto idx = spaceOrder_.indexOf(tagId.mid(6));
-                if (idx != -1)
-                        emit dataChanged(index(idx + 1), index(idx + 1), {Hidden});
-        }
-
-        emit hiddenTagsChanged();
+    if (hiddentTagIds_.contains(tagId)) {
+        hiddentTagIds_.removeOne(tagId);
+        UserSettings::instance()->setHiddenTags(hiddentTagIds_);
+    } else {
+        hiddentTagIds_.push_back(tagId);
+        UserSettings::instance()->setHiddenTags(hiddentTagIds_);
+    }
+
+    if (tagId.startsWith("tag:")) {
+        auto idx = tags_.indexOf(tagId.mid(4));
+        if (idx != -1)
+            emit dataChanged(
+              index(idx + 1 + spaceOrder_.size()), index(idx + 1 + spaceOrder_.size()), {Hidden});
+    } else if (tagId.startsWith("space:")) {
+        auto idx = spaceOrder_.indexOf(tagId.mid(6));
+        if (idx != -1)
+            emit dataChanged(index(idx + 1), index(idx + 1), {Hidden});
+    }
+
+    emit hiddenTagsChanged();
 }
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 677581dce22d8e09116290aa6ad0544e40181998..0440d17f2a6aedd2b6510993d7c50516901070d6 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -15,64 +15,64 @@
 
 class CommunitiesModel : public QAbstractListModel
 {
-        Q_OBJECT
-        Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY
-                     currentTagIdChanged RESET resetCurrentTagId)
-        Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged)
-        Q_PROPERTY(QStringList tagsWithDefault READ tagsWithDefault NOTIFY tagsChanged)
+    Q_OBJECT
+    Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY
+                 currentTagIdChanged RESET resetCurrentTagId)
+    Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged)
+    Q_PROPERTY(QStringList tagsWithDefault READ tagsWithDefault NOTIFY tagsChanged)
 
 public:
-        enum Roles
-        {
-                AvatarUrl = Qt::UserRole,
-                DisplayName,
-                Tooltip,
-                ChildrenHidden,
-                Hidden,
-                Id,
-        };
+    enum Roles
+    {
+        AvatarUrl = Qt::UserRole,
+        DisplayName,
+        Tooltip,
+        ChildrenHidden,
+        Hidden,
+        Id,
+    };
 
-        CommunitiesModel(QObject *parent = nullptr);
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override
-        {
-                (void)parent;
-                return 1 + tags_.size() + spaceOrder_.size();
-        }
-        QVariant data(const QModelIndex &index, int role) const override;
+    CommunitiesModel(QObject *parent = nullptr);
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        (void)parent;
+        return 1 + tags_.size() + spaceOrder_.size();
+    }
+    QVariant data(const QModelIndex &index, int role) const override;
 
 public slots:
-        void initializeSidebar();
-        void sync(const mtx::responses::Rooms &rooms);
-        void clear();
-        QString currentTagId() const { return currentTagId_; }
-        void setCurrentTagId(QString tagId);
-        void resetCurrentTagId()
-        {
-                currentTagId_.clear();
-                emit currentTagIdChanged(currentTagId_);
-        }
-        QStringList tags() const { return tags_; }
-        QStringList tagsWithDefault() const
-        {
-                QStringList tagsWD = tags_;
-                tagsWD.prepend("m.lowpriority");
-                tagsWD.prepend("m.favourite");
-                tagsWD.removeOne("m.server_notice");
-                tagsWD.removeDuplicates();
-                return tagsWD;
-        }
-        void toggleTagId(QString tagId);
+    void initializeSidebar();
+    void sync(const mtx::responses::Rooms &rooms);
+    void clear();
+    QString currentTagId() const { return currentTagId_; }
+    void setCurrentTagId(QString tagId);
+    void resetCurrentTagId()
+    {
+        currentTagId_.clear();
+        emit currentTagIdChanged(currentTagId_);
+    }
+    QStringList tags() const { return tags_; }
+    QStringList tagsWithDefault() const
+    {
+        QStringList tagsWD = tags_;
+        tagsWD.prepend("m.lowpriority");
+        tagsWD.prepend("m.favourite");
+        tagsWD.removeOne("m.server_notice");
+        tagsWD.removeDuplicates();
+        return tagsWD;
+    }
+    void toggleTagId(QString tagId);
 
 signals:
-        void currentTagIdChanged(QString tagId);
-        void hiddenTagsChanged();
-        void tagsChanged();
+    void currentTagIdChanged(QString tagId);
+    void hiddenTagsChanged();
+    void tagsChanged();
 
 private:
-        QStringList tags_;
-        QString currentTagId_;
-        QStringList hiddentTagIds_;
-        QStringList spaceOrder_;
-        std::map<QString, RoomInfo> spaces_;
+    QStringList tags_;
+    QString currentTagId_;
+    QStringList hiddentTagIds_;
+    QStringList spaceOrder_;
+    std::map<QString, RoomInfo> spaces_;
 };
diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp
index 39c8fa17b2c7f8a54375dd4dda627c794c8ded9d..682077ae26f75c392b909b1560dce432170f5190 100644
--- a/src/timeline/DelegateChooser.cpp
+++ b/src/timeline/DelegateChooser.cpp
@@ -13,127 +13,126 @@
 QQmlComponent *
 DelegateChoice::delegate() const
 {
-        return delegate_;
+    return delegate_;
 }
 
 void
 DelegateChoice::setDelegate(QQmlComponent *delegate)
 {
-        if (delegate != delegate_) {
-                delegate_ = delegate;
-                emit delegateChanged();
-                emit changed();
-        }
+    if (delegate != delegate_) {
+        delegate_ = delegate;
+        emit delegateChanged();
+        emit changed();
+    }
 }
 
 QVariant
 DelegateChoice::roleValue() const
 {
-        return roleValue_;
+    return roleValue_;
 }
 
 void
 DelegateChoice::setRoleValue(const QVariant &value)
 {
-        if (value != roleValue_) {
-                roleValue_ = value;
-                emit roleValueChanged();
-                emit changed();
-        }
+    if (value != roleValue_) {
+        roleValue_ = value;
+        emit roleValueChanged();
+        emit changed();
+    }
 }
 
 QVariant
 DelegateChooser::roleValue() const
 {
-        return roleValue_;
+    return roleValue_;
 }
 
 void
 DelegateChooser::setRoleValue(const QVariant &value)
 {
-        if (value != roleValue_) {
-                roleValue_ = value;
-                recalcChild();
-                emit roleValueChanged();
-        }
+    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);
+    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);
+    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();
+    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);
+    return static_cast<DelegateChooser *>(p->object)->choices_.at(index);
 }
 void
 DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p)
 {
-        static_cast<DelegateChooser *>(p->object)->choices_.clear();
+    static_cast<DelegateChooser *>(p->object)->choices_.clear();
 }
 
 void
 DelegateChooser::recalcChild()
 {
-        for (const auto choice : qAsConst(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;
-                }
+    for (const auto choice : qAsConst(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();
+    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);
-                QQmlEngine::setObjectOwnership(chooser.child_,
-                                               QQmlEngine::ObjectOwnership::JavaScriptOwnership);
-                emit chooser.childChanged();
-
-        } else if (status == QQmlIncubator::Error) {
-                for (const auto &e : errors())
-                        nhlog::ui()->error("Error instantiating delegate: {}",
-                                           e.toString().toStdString());
+    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);
+        QQmlEngine::setObjectOwnership(chooser.child_,
+                                       QQmlEngine::ObjectOwnership::JavaScriptOwnership);
+        emit chooser.childChanged();
+
+    } 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
index 22e423a2781cb4195f30173038bd26361c261507..3e4b16d7f98a13c66694eaa60c748679109ac4a8 100644
--- a/src/timeline/DelegateChooser.h
+++ b/src/timeline/DelegateChooser.h
@@ -18,73 +18,73 @@ class QQmlAdaptorModel;
 
 class DelegateChoice : public QObject
 {
-        Q_OBJECT
-        Q_CLASSINFO("DefaultProperty", "delegate")
+    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)
+    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);
+    QQmlComponent *delegate() const;
+    void setDelegate(QQmlComponent *delegate);
 
-        QVariant roleValue() const;
-        void setRoleValue(const QVariant &value);
+    QVariant roleValue() const;
+    void setRoleValue(const QVariant &value);
 
 signals:
-        void delegateChanged();
-        void roleValueChanged();
-        void changed();
+    void delegateChanged();
+    void roleValueChanged();
+    void changed();
 
 private:
-        QVariant roleValue_;
-        QQmlComponent *delegate_ = nullptr;
+    QVariant roleValue_;
+    QQmlComponent *delegate_ = nullptr;
 };
 
 class DelegateChooser : public QQuickItem
 {
-        Q_OBJECT
-        Q_CLASSINFO("DefaultProperty", "choices")
+    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)
-        Q_PROPERTY(QQuickItem *child READ child NOTIFY childChanged)
+    Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT)
+    Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged)
+    Q_PROPERTY(QQuickItem *child READ child NOTIFY childChanged)
 
-        QQmlListProperty<DelegateChoice> choices();
+    QQmlListProperty<DelegateChoice> choices();
 
-        QVariant roleValue() const;
-        void setRoleValue(const QVariant &value);
+    QVariant roleValue() const;
+    void setRoleValue(const QVariant &value);
 
-        QQuickItem *child() const { return child_; }
+    QQuickItem *child() const { return child_; }
 
-        void recalcChild();
-        void componentComplete() override;
+    void recalcChild();
+    void componentComplete() override;
 
 signals:
-        void roleChanged();
-        void roleValueChanged();
-        void childChanged();
+    void roleChanged();
+    void roleValueChanged();
+    void childChanged();
 
 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> *);
+    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/EventStore.cpp b/src/timeline/EventStore.cpp
index 742f8dbb25238ad5af81ed3c30dfdcdeb3c8310a..a1f4c67f331899d045dcbe47fec22ac552516368 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -15,7 +15,6 @@
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MatrixClient.h"
-#include "Olm.h"
 #include "Utils.h"
 
 Q_DECLARE_METATYPE(Reaction)
@@ -28,390 +27,373 @@ QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::
 EventStore::EventStore(std::string room_id, QObject *)
   : room_id_(std::move(room_id))
 {
-        static auto reactionType = qRegisterMetaType<Reaction>();
-        (void)reactionType;
-
-        auto range = cache::client()->getTimelineRange(room_id_);
-
-        if (range) {
-                this->first = range->first;
-                this->last  = range->last;
-        }
-
-        connect(
-          this,
-          &EventStore::eventFetched,
-          this,
-          [this](std::string id,
-                 std::string relatedTo,
-                 mtx::events::collections::TimelineEvents timeline) {
-                  cache::client()->storeEvent(room_id_, id, {timeline});
-
-                  if (!relatedTo.empty()) {
-                          auto idx = idToIndex(relatedTo);
-                          if (idx)
-                                  emit dataChanged(*idx, *idx);
-                  }
-          },
-          Qt::QueuedConnection);
-
-        connect(
-          this,
-          &EventStore::oldMessagesRetrieved,
-          this,
-          [this](const mtx::responses::Messages &res) {
-                  if (res.end.empty() || cache::client()->previousBatchToken(room_id_) == res.end) {
-                          noMoreMessages = true;
-                          emit fetchedMore();
-                          return;
+    static auto reactionType = qRegisterMetaType<Reaction>();
+    (void)reactionType;
+
+    auto range = cache::client()->getTimelineRange(room_id_);
+
+    if (range) {
+        this->first = range->first;
+        this->last  = range->last;
+    }
+
+    connect(
+      this,
+      &EventStore::eventFetched,
+      this,
+      [this](
+        std::string id, std::string relatedTo, mtx::events::collections::TimelineEvents timeline) {
+          cache::client()->storeEvent(room_id_, id, {timeline});
+
+          if (!relatedTo.empty()) {
+              auto idx = idToIndex(relatedTo);
+              if (idx)
+                  emit dataChanged(*idx, *idx);
+          }
+      },
+      Qt::QueuedConnection);
+
+    connect(
+      this,
+      &EventStore::oldMessagesRetrieved,
+      this,
+      [this](const mtx::responses::Messages &res) {
+          if (res.end.empty() || cache::client()->previousBatchToken(room_id_) == res.end) {
+              noMoreMessages = true;
+              emit fetchedMore();
+              return;
+          }
+
+          uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
+          if (newFirst == first)
+              fetchMore();
+          else {
+              if (this->last != std::numeric_limits<uint64_t>::max()) {
+                  auto oldFirst = this->first;
+                  emit beginInsertRows(toExternalIdx(newFirst), toExternalIdx(this->first - 1));
+                  this->first = newFirst;
+                  emit endInsertRows();
+                  emit fetchedMore();
+                  emit dataChanged(toExternalIdx(oldFirst), toExternalIdx(oldFirst));
+              } else {
+                  auto range = cache::client()->getTimelineRange(room_id_);
+
+                  if (range && range->last - range->first != 0) {
+                      emit beginInsertRows(0, int(range->last - range->first));
+                      this->first = range->first;
+                      this->last  = range->last;
+                      emit endInsertRows();
+                      emit fetchedMore();
+                  } else {
+                      fetchMore();
                   }
+              }
+          }
+      },
+      Qt::QueuedConnection);
+
+    connect(this, &EventStore::processPending, this, [this]() {
+        if (!current_txn.empty()) {
+            nhlog::ui()->debug("Already processing {}", current_txn);
+            return;
+        }
 
-                  uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
-                  if (newFirst == first)
-                          fetchMore();
-                  else {
-                          if (this->last != std::numeric_limits<uint64_t>::max()) {
-                                  emit beginInsertRows(toExternalIdx(newFirst),
-                                                       toExternalIdx(this->first - 1));
-                                  this->first = newFirst;
-                                  emit endInsertRows();
-                                  emit fetchedMore();
-                          } else {
-                                  auto range = cache::client()->getTimelineRange(room_id_);
-
-                                  if (range && range->last - range->first != 0) {
-                                          emit beginInsertRows(0, int(range->last - range->first));
-                                          this->first = range->first;
-                                          this->last  = range->last;
-                                          emit endInsertRows();
-                                          emit fetchedMore();
-                                  } else {
-                                          fetchMore();
-                                  }
-                          }
-                  }
-          },
-          Qt::QueuedConnection);
+        auto event = cache::client()->firstPendingMessage(room_id_);
 
-        connect(this, &EventStore::processPending, this, [this]() {
-                if (!current_txn.empty()) {
-                        nhlog::ui()->debug("Already processing {}", current_txn);
-                        return;
-                }
+        if (!event) {
+            nhlog::ui()->debug("No event to send");
+            return;
+        }
 
-                auto event = cache::client()->firstPendingMessage(room_id_);
+        std::visit(
+          [this](auto e) {
+              auto txn_id       = e.event_id;
+              this->current_txn = txn_id;
 
-                if (!event) {
-                        nhlog::ui()->debug("No event to send");
-                        return;
-                }
+              if (txn_id.empty() || txn_id[0] != 'm') {
+                  nhlog::ui()->debug("Invalid txn id '{}'", txn_id);
+                  cache::client()->removePendingStatus(room_id_, txn_id);
+                  return;
+              }
+
+              if constexpr (mtx::events::message_content_to_type<decltype(e.content)> !=
+                            mtx::events::EventType::Unsupported)
+                  http::client()->send_room_message(
+                    room_id_,
+                    txn_id,
+                    e.content,
+                    [this, txn_id, e](const mtx::responses::EventId &event_id,
+                                      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;
+                        }
 
-                std::visit(
-                  [this](auto e) {
-                          auto txn_id       = e.event_id;
-                          this->current_txn = txn_id;
-
-                          if (txn_id.empty() || txn_id[0] != 'm') {
-                                  nhlog::ui()->debug("Invalid txn id '{}'", txn_id);
-                                  cache::client()->removePendingStatus(room_id_, txn_id);
-                                  return;
-                          }
-
-                          if constexpr (mtx::events::message_content_to_type<decltype(e.content)> !=
-                                        mtx::events::EventType::Unsupported)
-                                  http::client()->send_room_message(
-                                    room_id_,
-                                    txn_id,
-                                    e.content,
-                                    [this, txn_id, e](const mtx::responses::EventId &event_id,
-                                                      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, event_id.event_id.to_string());
-                                            if constexpr (std::is_same_v<
-                                                            decltype(e.content),
-                                                            mtx::events::msg::Encrypted>) {
-                                                    auto event =
-                                                      decryptEvent({room_id_, e.event_id}, e);
-                                                    if (event->event) {
-                                                            if (auto dec = std::get_if<
-                                                                  mtx::events::RoomEvent<
-                                                                    mtx::events::msg::
-                                                                      KeyVerificationRequest>>(
-                                                                  &event->event.value())) {
-                                                                    emit updateFlowEventId(
-                                                                      event_id.event_id
-                                                                        .to_string());
-                                                            }
-                                                    }
-                                            }
-                                    });
-                  },
-                  event->data);
-        });
-
-        connect(
-          this,
-          &EventStore::messageFailed,
-          this,
-          [this](std::string txn_id) {
-                  if (current_txn == txn_id) {
-                          current_txn_error_count++;
-                          if (current_txn_error_count > 10) {
-                                  nhlog::ui()->debug("failing txn id '{}'", txn_id);
-                                  cache::client()->removePendingStatus(room_id_, txn_id);
-                                  current_txn_error_count = 0;
-                          }
-                  }
-                  QTimer::singleShot(1000, this, [this]() {
-                          nhlog::ui()->debug("timeout");
-                          this->current_txn = "";
-                          emit processPending();
-                  });
+                        emit messageSent(txn_id, event_id.event_id.to_string());
+                        if constexpr (std::is_same_v<decltype(e.content),
+                                                     mtx::events::msg::Encrypted>) {
+                            auto event = decryptEvent({room_id_, e.event_id}, e);
+                            if (event->event) {
+                                if (auto dec = std::get_if<mtx::events::RoomEvent<
+                                      mtx::events::msg::KeyVerificationRequest>>(
+                                      &event->event.value())) {
+                                    emit updateFlowEventId(event_id.event_id.to_string());
+                                }
+                            }
+                        }
+                    });
           },
-          Qt::QueuedConnection);
-
-        connect(
-          this,
-          &EventStore::messageSent,
-          this,
-          [this](std::string txn_id, std::string event_id) {
-                  nhlog::ui()->debug("sent {}", txn_id);
-
-                  // Replace the event_id in pending edits/replies/redactions with the actual
-                  // event_id of this event. This allows one to edit and reply to events that are
-                  // currently pending.
-
-                  // FIXME (introduced by balsoft): this doesn't work for encrypted events, but
-                  // allegedly it's hard to fix so I'll leave my first contribution at that
-                  for (auto related_event_id : cache::client()->relatedEvents(room_id_, txn_id)) {
-                          if (cache::client()->getEvent(room_id_, related_event_id)) {
-                                  auto related_event =
-                                    cache::client()->getEvent(room_id_, related_event_id).value();
-                                  auto relations = mtx::accessors::relations(related_event.data);
-
-                                  // Replace the blockquote in fallback reply
-                                  auto related_text =
-                                    std::get_if<mtx::events::RoomEvent<mtx::events::msg::Text>>(
-                                      &related_event.data);
-                                  if (related_text && relations.reply_to() == txn_id) {
-                                          size_t index =
-                                            related_text->content.formatted_body.find(txn_id);
-                                          if (index != std::string::npos) {
-                                                  related_text->content.formatted_body.replace(
-                                                    index, event_id.length(), event_id);
-                                          }
-                                  }
+          event->data);
+    });
+
+    connect(
+      this,
+      &EventStore::messageFailed,
+      this,
+      [this](std::string txn_id) {
+          if (current_txn == txn_id) {
+              current_txn_error_count++;
+              if (current_txn_error_count > 10) {
+                  nhlog::ui()->debug("failing txn id '{}'", txn_id);
+                  cache::client()->removePendingStatus(room_id_, txn_id);
+                  current_txn_error_count = 0;
+              }
+          }
+          QTimer::singleShot(1000, this, [this]() {
+              nhlog::ui()->debug("timeout");
+              this->current_txn = "";
+              emit processPending();
+          });
+      },
+      Qt::QueuedConnection);
+
+    connect(
+      this,
+      &EventStore::messageSent,
+      this,
+      [this](std::string txn_id, std::string event_id) {
+          nhlog::ui()->debug("sent {}", txn_id);
+
+          // Replace the event_id in pending edits/replies/redactions with the actual
+          // event_id of this event. This allows one to edit and reply to events that are
+          // currently pending.
+
+          // FIXME (introduced by balsoft): this doesn't work for encrypted events, but
+          // allegedly it's hard to fix so I'll leave my first contribution at that
+          for (auto related_event_id : cache::client()->relatedEvents(room_id_, txn_id)) {
+              if (cache::client()->getEvent(room_id_, related_event_id)) {
+                  auto related_event =
+                    cache::client()->getEvent(room_id_, related_event_id).value();
+                  auto relations = mtx::accessors::relations(related_event.data);
+
+                  // Replace the blockquote in fallback reply
+                  auto related_text = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Text>>(
+                    &related_event.data);
+                  if (related_text && relations.reply_to() == txn_id) {
+                      size_t index = related_text->content.formatted_body.find(txn_id);
+                      if (index != std::string::npos) {
+                          related_text->content.formatted_body.replace(
+                            index, event_id.length(), event_id);
+                      }
+                  }
 
-                                  for (mtx::common::Relation &rel : relations.relations) {
-                                          if (rel.event_id == txn_id)
-                                                  rel.event_id = event_id;
-                                  }
+                  for (mtx::common::Relation &rel : relations.relations) {
+                      if (rel.event_id == txn_id)
+                          rel.event_id = event_id;
+                  }
 
-                                  mtx::accessors::set_relations(related_event.data, relations);
+                  mtx::accessors::set_relations(related_event.data, relations);
 
-                                  cache::client()->replaceEvent(
-                                    room_id_, related_event_id, related_event);
+                  cache::client()->replaceEvent(room_id_, related_event_id, related_event);
 
-                                  auto idx = idToIndex(related_event_id);
+                  auto idx = idToIndex(related_event_id);
 
-                                  events_by_id_.remove({room_id_, related_event_id});
-                                  events_.remove({room_id_, toInternalIdx(*idx)});
-                          }
-                  }
+                  events_by_id_.remove({room_id_, related_event_id});
+                  events_.remove({room_id_, toInternalIdx(*idx)});
+              }
+          }
 
-                  http::client()->read_event(
-                    room_id_, event_id, [this, event_id](mtx::http::RequestErr err) {
-                            if (err) {
-                                    nhlog::net()->warn(
-                                      "failed to read_event ({}, {})", room_id_, event_id);
-                            }
-                    });
+          http::client()->read_event(
+            room_id_, event_id, [this, event_id](mtx::http::RequestErr err) {
+                if (err) {
+                    nhlog::net()->warn("failed to read_event ({}, {})", room_id_, event_id);
+                }
+            });
 
-                  auto idx = idToIndex(event_id);
+          auto idx = idToIndex(event_id);
 
-                  if (idx)
-                          emit dataChanged(*idx, *idx);
+          if (idx)
+              emit dataChanged(*idx, *idx);
 
-                  cache::client()->removePendingStatus(room_id_, txn_id);
-                  this->current_txn             = "";
-                  this->current_txn_error_count = 0;
-                  emit processPending();
-          },
-          Qt::QueuedConnection);
+          cache::client()->removePendingStatus(room_id_, txn_id);
+          this->current_txn             = "";
+          this->current_txn_error_count = 0;
+          emit processPending();
+      },
+      Qt::QueuedConnection);
 }
 
 void
 EventStore::addPending(mtx::events::collections::TimelineEvents event)
 {
-        if (this->thread() != QThread::currentThread())
-                nhlog::db()->warn("{} called from a different thread!", __func__);
+    if (this->thread() != QThread::currentThread())
+        nhlog::db()->warn("{} called from a different thread!", __func__);
 
-        cache::client()->savePendingMessage(this->room_id_, {event});
-        mtx::responses::Timeline events;
-        events.limited = false;
-        events.events.emplace_back(event);
-        handleSync(events);
+    cache::client()->savePendingMessage(this->room_id_, {event});
+    mtx::responses::Timeline events;
+    events.limited = false;
+    events.events.emplace_back(event);
+    handleSync(events);
 
-        emit processPending();
+    emit processPending();
 }
 
 void
 EventStore::clearTimeline()
 {
-        emit beginResetModel();
-
-        cache::client()->clearTimeline(room_id_);
-        auto range = cache::client()->getTimelineRange(room_id_);
-        if (range) {
-                nhlog::db()->info("Range {} {}", range->last, range->first);
-                this->last  = range->last;
-                this->first = range->first;
-        } else {
-                this->first = std::numeric_limits<uint64_t>::max();
-                this->last  = std::numeric_limits<uint64_t>::max();
-        }
-        nhlog::ui()->info("Range {} {}", this->last, this->first);
-
-        decryptedEvents_.clear();
-        events_.clear();
-
-        emit endResetModel();
+    emit beginResetModel();
+
+    cache::client()->clearTimeline(room_id_);
+    auto range = cache::client()->getTimelineRange(room_id_);
+    if (range) {
+        nhlog::db()->info("Range {} {}", range->last, range->first);
+        this->last  = range->last;
+        this->first = range->first;
+    } else {
+        this->first = std::numeric_limits<uint64_t>::max();
+        this->last  = std::numeric_limits<uint64_t>::max();
+    }
+    nhlog::ui()->info("Range {} {}", this->last, this->first);
+
+    decryptedEvents_.clear();
+    events_.clear();
+
+    emit endResetModel();
 }
 
 void
 EventStore::receivedSessionKey(const std::string &session_id)
 {
-        if (!pending_key_requests.count(session_id))
-                return;
+    if (!pending_key_requests.count(session_id))
+        return;
 
-        auto request = pending_key_requests.at(session_id);
+    auto request = pending_key_requests.at(session_id);
 
-        // Don't request keys again until Nheko is restarted (for now)
-        pending_key_requests[session_id].events.clear();
+    // Don't request keys again until Nheko is restarted (for now)
+    pending_key_requests[session_id].events.clear();
 
-        if (!request.events.empty())
-                olm::send_key_request_for(request.events.front(), request.request_id, true);
+    if (!request.events.empty())
+        olm::send_key_request_for(request.events.front(), request.request_id, true);
 
-        for (const auto &e : request.events) {
-                auto idx = idToIndex(e.event_id);
-                if (idx) {
-                        decryptedEvents_.remove({room_id_, e.event_id});
-                        events_by_id_.remove({room_id_, e.event_id});
-                        events_.remove({room_id_, toInternalIdx(*idx)});
-                        emit dataChanged(*idx, *idx);
-                }
+    for (const auto &e : request.events) {
+        auto idx = idToIndex(e.event_id);
+        if (idx) {
+            decryptedEvents_.remove({room_id_, e.event_id});
+            events_by_id_.remove({room_id_, e.event_id});
+            events_.remove({room_id_, toInternalIdx(*idx)});
+            emit dataChanged(*idx, *idx);
         }
+    }
 }
 
 void
 EventStore::handleSync(const mtx::responses::Timeline &events)
 {
-        if (this->thread() != QThread::currentThread())
-                nhlog::db()->warn("{} called from a different thread!", __func__);
-
-        auto range = cache::client()->getTimelineRange(room_id_);
-        if (!range) {
-                emit beginResetModel();
-                this->first = std::numeric_limits<uint64_t>::max();
-                this->last  = std::numeric_limits<uint64_t>::max();
-
-                decryptedEvents_.clear();
-                events_.clear();
-                emit endResetModel();
-                return;
-        }
+    if (this->thread() != QThread::currentThread())
+        nhlog::db()->warn("{} called from a different thread!", __func__);
 
-        if (events.limited) {
-                emit beginResetModel();
-                this->last  = range->last;
-                this->first = range->first;
-
-                decryptedEvents_.clear();
-                events_.clear();
-                emit endResetModel();
-        } else if (range->last > this->last) {
-                emit beginInsertRows(toExternalIdx(this->last + 1), toExternalIdx(range->last));
-                this->last = range->last;
-                emit endInsertRows();
-        }
+    auto range = cache::client()->getTimelineRange(room_id_);
+    if (!range) {
+        emit beginResetModel();
+        this->first = std::numeric_limits<uint64_t>::max();
+        this->last  = std::numeric_limits<uint64_t>::max();
 
-        for (const auto &event : events.events) {
-                std::set<std::string> relates_to;
-                if (auto redaction =
-                      std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(
-                        &event)) {
-                        // fixup reactions
-                        auto redacted = events_by_id_.object({room_id_, redaction->redacts});
-                        if (redacted) {
-                                auto id = mtx::accessors::relations(*redacted);
-                                if (id.annotates()) {
-                                        auto idx = idToIndex(id.annotates()->event_id);
-                                        if (idx) {
-                                                events_by_id_.remove(
-                                                  {room_id_, redaction->redacts});
-                                                events_.remove({room_id_, toInternalIdx(*idx)});
-                                                emit dataChanged(*idx, *idx);
-                                        }
-                                }
-                        }
+        decryptedEvents_.clear();
+        events_.clear();
+        emit endResetModel();
+        return;
+    }
 
-                        relates_to.insert(redaction->redacts);
-                } else {
-                        for (const auto &r : mtx::accessors::relations(event).relations)
-                                relates_to.insert(r.event_id);
-                }
+    if (events.limited) {
+        emit beginResetModel();
+        this->last  = range->last;
+        this->first = range->first;
 
-                for (const auto &relates_to_id : relates_to) {
-                        auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id);
-                        if (idx) {
-                                events_by_id_.remove({room_id_, relates_to_id});
-                                decryptedEvents_.remove({room_id_, relates_to_id});
-                                events_.remove({room_id_, *idx});
-                                emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
-                        }
+        decryptedEvents_.clear();
+        events_.clear();
+        emit endResetModel();
+    } else if (range->last > this->last) {
+        emit beginInsertRows(toExternalIdx(this->last + 1), toExternalIdx(range->last));
+        this->last = range->last;
+        emit endInsertRows();
+    }
+
+    for (const auto &event : events.events) {
+        std::set<std::string> relates_to;
+        if (auto redaction =
+              std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&event)) {
+            // fixup reactions
+            auto redacted = events_by_id_.object({room_id_, redaction->redacts});
+            if (redacted) {
+                auto id = mtx::accessors::relations(*redacted);
+                if (id.annotates()) {
+                    auto idx = idToIndex(id.annotates()->event_id);
+                    if (idx) {
+                        events_by_id_.remove({room_id_, redaction->redacts});
+                        events_.remove({room_id_, toInternalIdx(*idx)});
+                        emit dataChanged(*idx, *idx);
+                    }
                 }
+            }
 
-                if (auto txn_id = mtx::accessors::transaction_id(event); !txn_id.empty()) {
-                        auto idx = cache::client()->getTimelineIndex(
-                          room_id_, mtx::accessors::event_id(event));
-                        if (idx) {
-                                Index index{room_id_, *idx};
-                                events_.remove(index);
-                                emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
-                        }
-                }
+            relates_to.insert(redaction->redacts);
+        } else {
+            for (const auto &r : mtx::accessors::relations(event).relations)
+                relates_to.insert(r.event_id);
+        }
 
-                // decrypting and checking some encrypted messages
-                if (auto encrypted =
-                      std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                        &event)) {
-                        auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
-                        if (d_event->event &&
-                            std::visit(
-                              [](auto e) { return (e.sender != utils::localUser().toStdString()); },
-                              *d_event->event)) {
-                                handle_room_verification(*d_event->event);
-                        }
-                }
+        for (const auto &relates_to_id : relates_to) {
+            auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id);
+            if (idx) {
+                events_by_id_.remove({room_id_, relates_to_id});
+                decryptedEvents_.remove({room_id_, relates_to_id});
+                events_.remove({room_id_, *idx});
+                emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
+            }
+        }
+
+        if (auto txn_id = mtx::accessors::transaction_id(event); !txn_id.empty()) {
+            auto idx = cache::client()->getTimelineIndex(room_id_, mtx::accessors::event_id(event));
+            if (idx) {
+                Index index{room_id_, *idx};
+                events_.remove(index);
+                emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
+            }
         }
+
+        // decrypting and checking some encrypted messages
+        if (auto encrypted =
+              std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+            auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+            if (d_event->event &&
+                std::visit([](auto e) { return (e.sender != utils::localUser().toStdString()); },
+                           *d_event->event)) {
+                handle_room_verification(*d_event->event);
+            }
+        }
+    }
 }
 
 namespace {
 template<class... Ts>
 struct overloaded : Ts...
 {
-        using Ts::operator()...;
+    using Ts::operator()...;
 };
 template<class... Ts>
 overloaded(Ts...) -> overloaded<Ts...>;
@@ -420,463 +402,453 @@ overloaded(Ts...) -> overloaded<Ts...>;
 void
 EventStore::handle_room_verification(mtx::events::collections::TimelineEvents event)
 {
-        std::visit(
-          overloaded{
-            [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Request");
-                    emit startDMVerification(msg);
-            },
-            [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Cancel");
-                    ChatPage::instance()->receivedDeviceVerificationCancel(msg.content);
-            },
-            [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Accept");
-                    ChatPage::instance()->receivedDeviceVerificationAccept(msg.content);
-            },
-            [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Key");
-                    ChatPage::instance()->receivedDeviceVerificationKey(msg.content);
-            },
-            [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Mac");
-                    ChatPage::instance()->receivedDeviceVerificationMac(msg.content);
-            },
-            [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Ready");
-                    ChatPage::instance()->receivedDeviceVerificationReady(msg.content);
-            },
-            [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Done");
-                    ChatPage::instance()->receivedDeviceVerificationDone(msg.content);
-            },
-            [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) {
-                    nhlog::db()->debug("handle_room_verification: Start");
-                    ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender);
-            },
-            [](const auto &) {},
-          },
-          event);
+    std::visit(
+      overloaded{
+        [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) {
+            nhlog::db()->debug("handle_room_verification: Request");
+            emit startDMVerification(msg);
+        },
+        [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) {
+            nhlog::db()->debug("handle_room_verification: Cancel");
+            ChatPage::instance()->receivedDeviceVerificationCancel(msg.content);
+        },
+        [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) {
+            nhlog::db()->debug("handle_room_verification: Accept");
+            ChatPage::instance()->receivedDeviceVerificationAccept(msg.content);
+        },
+        [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) {
+            nhlog::db()->debug("handle_room_verification: Key");
+            ChatPage::instance()->receivedDeviceVerificationKey(msg.content);
+        },
+        [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) {
+            nhlog::db()->debug("handle_room_verification: Mac");
+            ChatPage::instance()->receivedDeviceVerificationMac(msg.content);
+        },
+        [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) {
+            nhlog::db()->debug("handle_room_verification: Ready");
+            ChatPage::instance()->receivedDeviceVerificationReady(msg.content);
+        },
+        [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) {
+            nhlog::db()->debug("handle_room_verification: Done");
+            ChatPage::instance()->receivedDeviceVerificationDone(msg.content);
+        },
+        [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) {
+            nhlog::db()->debug("handle_room_verification: Start");
+            ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender);
+        },
+        [](const auto &) {},
+      },
+      event);
 }
 
 std::vector<mtx::events::collections::TimelineEvents>
 EventStore::edits(const std::string &event_id)
 {
-        auto event_ids = cache::client()->relatedEvents(room_id_, event_id);
-
-        auto original_event = get(event_id, "", false, false);
-        if (!original_event)
-                return {};
-
-        auto original_sender    = mtx::accessors::sender(*original_event);
-        auto original_relations = mtx::accessors::relations(*original_event);
-
-        std::vector<mtx::events::collections::TimelineEvents> edits;
-        for (const auto &id : event_ids) {
-                auto related_event = get(id, event_id, false, false);
-                if (!related_event)
-                        continue;
-
-                auto related_ev = *related_event;
-
-                auto edit_rel = mtx::accessors::relations(related_ev);
-                if (edit_rel.replaces() == event_id &&
-                    original_sender == mtx::accessors::sender(related_ev)) {
-                        if (edit_rel.synthesized && original_relations.reply_to() &&
-                            !edit_rel.reply_to()) {
-                                edit_rel.relations.push_back(
-                                  {mtx::common::RelationType::InReplyTo,
-                                   original_relations.reply_to().value()});
-                                mtx::accessors::set_relations(related_ev, std::move(edit_rel));
-                        }
-                        edits.push_back(std::move(related_ev));
-                }
+    auto event_ids = cache::client()->relatedEvents(room_id_, event_id);
+
+    auto original_event = get(event_id, "", false, false);
+    if (!original_event)
+        return {};
+
+    auto original_sender    = mtx::accessors::sender(*original_event);
+    auto original_relations = mtx::accessors::relations(*original_event);
+
+    std::vector<mtx::events::collections::TimelineEvents> edits;
+    for (const auto &id : event_ids) {
+        auto related_event = get(id, event_id, false, false);
+        if (!related_event)
+            continue;
+
+        auto related_ev = *related_event;
+
+        auto edit_rel = mtx::accessors::relations(related_ev);
+        if (edit_rel.replaces() == event_id &&
+            original_sender == mtx::accessors::sender(related_ev)) {
+            if (edit_rel.synthesized && original_relations.reply_to() && !edit_rel.reply_to()) {
+                edit_rel.relations.push_back(
+                  {mtx::common::RelationType::InReplyTo, original_relations.reply_to().value()});
+                mtx::accessors::set_relations(related_ev, std::move(edit_rel));
+            }
+            edits.push_back(std::move(related_ev));
         }
-
-        auto c = cache::client();
-        std::sort(edits.begin(),
-                  edits.end(),
-                  [this, c](const mtx::events::collections::TimelineEvents &a,
-                            const mtx::events::collections::TimelineEvents &b) {
-                          return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) <
-                                 c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b));
-                  });
-
-        return edits;
+    }
+
+    auto c = cache::client();
+    std::sort(edits.begin(),
+              edits.end(),
+              [this, c](const mtx::events::collections::TimelineEvents &a,
+                        const mtx::events::collections::TimelineEvents &b) {
+                  return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) <
+                         c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b));
+              });
+
+    return edits;
 }
 
 QVariantList
 EventStore::reactions(const std::string &event_id)
 {
-        auto event_ids = cache::client()->relatedEvents(room_id_, event_id);
-
-        struct TempReaction
-        {
-                int count = 0;
-                std::vector<std::string> users;
-                std::string reactedBySelf;
-        };
-        std::map<std::string, TempReaction> aggregation;
-        std::vector<Reaction> reactions;
-
-        auto self = http::client()->user_id().to_string();
-        for (const auto &id : event_ids) {
-                auto related_event = get(id, event_id);
-                if (!related_event)
-                        continue;
-
-                if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
-                      related_event);
-                    reaction && reaction->content.relations.annotates() &&
-                    reaction->content.relations.annotates()->key) {
-                        auto key  = reaction->content.relations.annotates()->key.value();
-                        auto &agg = aggregation[key];
-
-                        if (agg.count == 0) {
-                                Reaction temp{};
-                                temp.key_ = QString::fromStdString(key);
-                                reactions.push_back(temp);
-                        }
-
-                        agg.count++;
-                        agg.users.push_back(cache::displayName(room_id_, reaction->sender));
-                        if (reaction->sender == self)
-                                agg.reactedBySelf = reaction->event_id;
-                }
+    auto event_ids = cache::client()->relatedEvents(room_id_, event_id);
+
+    struct TempReaction
+    {
+        int count = 0;
+        std::vector<std::string> users;
+        std::string reactedBySelf;
+    };
+    std::map<std::string, TempReaction> aggregation;
+    std::vector<Reaction> reactions;
+
+    auto self = http::client()->user_id().to_string();
+    for (const auto &id : event_ids) {
+        auto related_event = get(id, event_id);
+        if (!related_event)
+            continue;
+
+        if (auto reaction =
+              std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(related_event);
+            reaction && reaction->content.relations.annotates() &&
+            reaction->content.relations.annotates()->key) {
+            auto key  = reaction->content.relations.annotates()->key.value();
+            auto &agg = aggregation[key];
+
+            if (agg.count == 0) {
+                Reaction temp{};
+                temp.key_ = QString::fromStdString(key);
+                reactions.push_back(temp);
+            }
+
+            agg.count++;
+            agg.users.push_back(cache::displayName(room_id_, reaction->sender));
+            if (reaction->sender == self)
+                agg.reactedBySelf = reaction->event_id;
         }
-
-        QVariantList temp;
-        for (auto &reaction : reactions) {
-                const auto &agg            = aggregation[reaction.key_.toStdString()];
-                reaction.count_            = agg.count;
-                reaction.selfReactedEvent_ = QString::fromStdString(agg.reactedBySelf);
-
-                bool firstReaction = true;
-                for (const auto &user : agg.users) {
-                        if (firstReaction)
-                                firstReaction = false;
-                        else
-                                reaction.users_ += ", ";
-
-                        reaction.users_ += QString::fromStdString(user);
-                }
-
-                nhlog::db()->debug("key: {}, count: {}, users: {}",
-                                   reaction.key_.toStdString(),
-                                   reaction.count_,
-                                   reaction.users_.toStdString());
-                temp.append(QVariant::fromValue(reaction));
+    }
+
+    QVariantList temp;
+    for (auto &reaction : reactions) {
+        const auto &agg            = aggregation[reaction.key_.toStdString()];
+        reaction.count_            = agg.count;
+        reaction.selfReactedEvent_ = QString::fromStdString(agg.reactedBySelf);
+
+        bool firstReaction = true;
+        for (const auto &user : agg.users) {
+            if (firstReaction)
+                firstReaction = false;
+            else
+                reaction.users_ += ", ";
+
+            reaction.users_ += QString::fromStdString(user);
         }
 
-        return temp;
+        nhlog::db()->debug("key: {}, count: {}, users: {}",
+                           reaction.key_.toStdString(),
+                           reaction.count_,
+                           reaction.users_.toStdString());
+        temp.append(QVariant::fromValue(reaction));
+    }
+
+    return temp;
 }
 
 mtx::events::collections::TimelineEvents *
 EventStore::get(int idx, bool decrypt)
 {
-        if (this->thread() != QThread::currentThread())
-                nhlog::db()->warn("{} called from a different thread!", __func__);
-
-        Index index{room_id_, toInternalIdx(idx)};
-        if (index.idx > last || index.idx < first)
-                return nullptr;
-
-        auto event_ptr = events_.object(index);
-        if (!event_ptr) {
-                auto event_id = cache::client()->getTimelineEventId(room_id_, index.idx);
-                if (!event_id)
-                        return nullptr;
-
-                std::optional<mtx::events::collections::TimelineEvent> event;
-                auto edits_ = edits(*event_id);
-                if (edits_.empty())
-                        event = cache::client()->getEvent(room_id_, *event_id);
-                else
-                        event = {edits_.back()};
-
-                if (!event)
-                        return nullptr;
-                else
-                        event_ptr =
-                          new mtx::events::collections::TimelineEvents(std::move(event->data));
-                events_.insert(index, event_ptr);
-        }
+    if (this->thread() != QThread::currentThread())
+        nhlog::db()->warn("{} called from a different thread!", __func__);
+
+    Index index{room_id_, toInternalIdx(idx)};
+    if (index.idx > last || index.idx < first)
+        return nullptr;
+
+    auto event_ptr = events_.object(index);
+    if (!event_ptr) {
+        auto event_id = cache::client()->getTimelineEventId(room_id_, index.idx);
+        if (!event_id)
+            return nullptr;
+
+        std::optional<mtx::events::collections::TimelineEvent> event;
+        auto edits_ = edits(*event_id);
+        if (edits_.empty())
+            event = cache::client()->getEvent(room_id_, *event_id);
+        else
+            event = {edits_.back()};
 
-        if (decrypt) {
-                if (auto encrypted =
-                      std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                        event_ptr)) {
-                        auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
-                        if (decrypted->event)
-                                return &*decrypted->event;
-                }
+        if (!event)
+            return nullptr;
+        else
+            event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
+        events_.insert(index, event_ptr);
+    }
+
+    if (decrypt) {
+        if (auto encrypted =
+              std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
+            auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+            if (decrypted->event)
+                return &*decrypted->event;
         }
+    }
 
-        return event_ptr;
+    return event_ptr;
 }
 
 std::optional<int>
 EventStore::idToIndex(std::string_view id) const
 {
-        if (this->thread() != QThread::currentThread())
-                nhlog::db()->warn("{} called from a different thread!", __func__);
-
-        auto idx = cache::client()->getTimelineIndex(room_id_, id);
-        if (idx)
-                return toExternalIdx(*idx);
-        else
-                return std::nullopt;
+    if (this->thread() != QThread::currentThread())
+        nhlog::db()->warn("{} called from a different thread!", __func__);
+
+    auto idx = cache::client()->getTimelineIndex(room_id_, id);
+    if (idx)
+        return toExternalIdx(*idx);
+    else
+        return std::nullopt;
 }
 std::optional<std::string>
 EventStore::indexToId(int idx) const
 {
-        if (this->thread() != QThread::currentThread())
-                nhlog::db()->warn("{} called from a different thread!", __func__);
+    if (this->thread() != QThread::currentThread())
+        nhlog::db()->warn("{} called from a different thread!", __func__);
 
-        return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
+    return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
 }
 
 olm::DecryptionResult *
 EventStore::decryptEvent(const IdIndex &idx,
                          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
 {
-        if (auto cachedEvent = decryptedEvents_.object(idx))
-                return cachedEvent;
-
-        MegolmSessionIndex index;
-        index.room_id    = room_id_;
-        index.session_id = e.content.session_id;
-        index.sender_key = e.content.sender_key;
-
-        auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
-                auto event_ptr = new olm::DecryptionResult(std::move(event));
-                decryptedEvents_.insert(idx, event_ptr);
-                return event_ptr;
-        };
-
-        auto decryptionResult = olm::decryptEvent(index, e);
-
-        if (decryptionResult.error) {
-                switch (decryptionResult.error) {
-                case olm::DecryptionErrorCode::MissingSession:
-                case olm::DecryptionErrorCode::MissingSessionIndex: {
-                        nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
-                                              index.room_id,
-                                              index.session_id,
-                                              e.sender);
-
-                        requestSession(e, false);
-                        break;
-                }
-                case olm::DecryptionErrorCode::DbError:
-                        nhlog::db()->critical(
-                          "failed to retrieve megolm session with index ({}, {}, {})",
-                          index.room_id,
-                          index.session_id,
-                          index.sender_key,
-                          decryptionResult.error_message.value_or(""));
-                        break;
-                case olm::DecryptionErrorCode::DecryptionFailed:
-                        nhlog::crypto()->critical(
-                          "failed to decrypt message with index ({}, {}, {}): {}",
-                          index.room_id,
-                          index.session_id,
-                          index.sender_key,
-                          decryptionResult.error_message.value_or(""));
-                        break;
-                case olm::DecryptionErrorCode::ParsingFailed:
-                        break;
-                case olm::DecryptionErrorCode::ReplayAttack:
-                        nhlog::crypto()->critical(
-                          "Reply attack while decryptiong event {} in room {} from {}!",
-                          e.event_id,
-                          room_id_,
-                          index.sender_key);
-                        break;
-                case olm::DecryptionErrorCode::NoError:
-                        // unreachable
-                        break;
-                }
-                return asCacheEntry(std::move(decryptionResult));
-        }
+    if (auto cachedEvent = decryptedEvents_.object(idx))
+        return cachedEvent;
+
+    MegolmSessionIndex index(room_id_, e.content);
+
+    auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
+        auto event_ptr = new olm::DecryptionResult(std::move(event));
+        decryptedEvents_.insert(idx, event_ptr);
+        return event_ptr;
+    };
 
-        auto encInfo = mtx::accessors::file(decryptionResult.event.value());
-        if (encInfo)
-                emit newEncryptedImage(encInfo.value());
+    auto decryptionResult = olm::decryptEvent(index, e);
 
+    if (decryptionResult.error) {
+        switch (decryptionResult.error) {
+        case olm::DecryptionErrorCode::MissingSession:
+        case olm::DecryptionErrorCode::MissingSessionIndex: {
+            nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
+                                  index.room_id,
+                                  index.session_id,
+                                  e.sender);
+
+            requestSession(e, false);
+            break;
+        }
+        case olm::DecryptionErrorCode::DbError:
+            nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
+                                  index.room_id,
+                                  index.session_id,
+                                  index.sender_key,
+                                  decryptionResult.error_message.value_or(""));
+            break;
+        case olm::DecryptionErrorCode::DecryptionFailed:
+            nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
+                                      index.room_id,
+                                      index.session_id,
+                                      index.sender_key,
+                                      decryptionResult.error_message.value_or(""));
+            break;
+        case olm::DecryptionErrorCode::ParsingFailed:
+            break;
+        case olm::DecryptionErrorCode::ReplayAttack:
+            nhlog::crypto()->critical("Reply attack while decryptiong event {} in room {} from {}!",
+                                      e.event_id,
+                                      room_id_,
+                                      index.sender_key);
+            break;
+        case olm::DecryptionErrorCode::NoError:
+            // unreachable
+            break;
+        }
         return asCacheEntry(std::move(decryptionResult));
+    }
+
+    auto encInfo = mtx::accessors::file(decryptionResult.event.value());
+    if (encInfo)
+        emit newEncryptedImage(encInfo.value());
+
+    return asCacheEntry(std::move(decryptionResult));
 }
 
 void
 EventStore::requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
                            bool manual)
 {
-        // we may not want to request keys during initial sync and such
-        if (suppressKeyRequests)
-                return;
-
-        // TODO: Look in key backup
-        auto copy    = ev;
-        copy.room_id = room_id_;
-        if (pending_key_requests.count(ev.content.session_id)) {
-                auto &r = pending_key_requests.at(ev.content.session_id);
-                r.events.push_back(copy);
-
-                // automatically request once every 10 min, manually every 1 min
-                qint64 delay = manual ? 60 : (60 * 10);
-                if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) {
-                        r.requested_at = QDateTime::currentSecsSinceEpoch();
-                        olm::send_key_request_for(copy, r.request_id);
-                }
-        } else {
-                PendingKeyRequests request;
-                request.request_id   = "key_request." + http::client()->generate_txn_id();
-                request.requested_at = QDateTime::currentSecsSinceEpoch();
-                request.events.push_back(copy);
-                olm::send_key_request_for(copy, request.request_id);
-                pending_key_requests[ev.content.session_id] = request;
+    // we may not want to request keys during initial sync and such
+    if (suppressKeyRequests)
+        return;
+
+    // TODO: Look in key backup
+    auto copy    = ev;
+    copy.room_id = room_id_;
+    if (pending_key_requests.count(ev.content.session_id)) {
+        auto &r = pending_key_requests.at(ev.content.session_id);
+        r.events.push_back(copy);
+
+        // automatically request once every 10 min, manually every 1 min
+        qint64 delay = manual ? 60 : (60 * 10);
+        if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) {
+            r.requested_at = QDateTime::currentSecsSinceEpoch();
+            olm::lookup_keybackup(room_id_, ev.content.session_id);
+            olm::send_key_request_for(copy, r.request_id);
         }
+    } else {
+        PendingKeyRequests request;
+        request.request_id   = "key_request." + http::client()->generate_txn_id();
+        request.requested_at = QDateTime::currentSecsSinceEpoch();
+        request.events.push_back(copy);
+        olm::lookup_keybackup(room_id_, ev.content.session_id);
+        olm::send_key_request_for(copy, request.request_id);
+        pending_key_requests[ev.content.session_id] = request;
+    }
 }
 
 void
 EventStore::enableKeyRequests(bool suppressKeyRequests_)
 {
-        if (!suppressKeyRequests_) {
-                for (const auto &key : decryptedEvents_.keys())
-                        if (key.room == this->room_id_)
-                                decryptedEvents_.remove(key);
-                suppressKeyRequests = false;
-        } else
-                suppressKeyRequests = true;
+    if (!suppressKeyRequests_) {
+        for (const auto &key : decryptedEvents_.keys())
+            if (key.room == this->room_id_)
+                decryptedEvents_.remove(key);
+        suppressKeyRequests = false;
+    } else
+        suppressKeyRequests = true;
 }
 
 mtx::events::collections::TimelineEvents *
 EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits)
 {
-        if (this->thread() != QThread::currentThread())
-                nhlog::db()->warn("{} called from a different thread!", __func__);
-
-        if (id.empty())
-                return nullptr;
-
-        IdIndex index{room_id_, std::move(id)};
-        if (resolve_edits) {
-                auto edits_ = edits(index.id);
-                if (!edits_.empty()) {
-                        index.id = mtx::accessors::event_id(edits_.back());
-                        auto event_ptr =
-                          new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
-                        events_by_id_.insert(index, event_ptr);
-                }
-        }
+    if (this->thread() != QThread::currentThread())
+        nhlog::db()->warn("{} called from a different thread!", __func__);
 
-        auto event_ptr = events_by_id_.object(index);
-        if (!event_ptr) {
-                auto event = cache::client()->getEvent(room_id_, index.id);
-                if (!event) {
-                        http::client()->get_event(
-                          room_id_,
-                          index.id,
-                          [this, relatedTo = std::string(related_to), id = index.id](
-                            const mtx::events::collections::TimelineEvents &timeline,
-                            mtx::http::RequestErr err) {
-                                  if (err) {
-                                          nhlog::net()->error(
-                                            "Failed to retrieve event with id {}, which was "
-                                            "requested to show the replyTo for event {}",
-                                            relatedTo,
-                                            id);
-                                          return;
-                                  }
-                                  emit eventFetched(id, relatedTo, timeline);
-                          });
-                        return nullptr;
-                }
-                event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
-                events_by_id_.insert(index, event_ptr);
+    if (id.empty())
+        return nullptr;
+
+    IdIndex index{room_id_, std::move(id)};
+    if (resolve_edits) {
+        auto edits_ = edits(index.id);
+        if (!edits_.empty()) {
+            index.id       = mtx::accessors::event_id(edits_.back());
+            auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
+            events_by_id_.insert(index, event_ptr);
         }
+    }
+
+    auto event_ptr = events_by_id_.object(index);
+    if (!event_ptr) {
+        auto event = cache::client()->getEvent(room_id_, index.id);
+        if (!event) {
+            http::client()->get_event(room_id_,
+                                      index.id,
+                                      [this, relatedTo = std::string(related_to), id = index.id](
+                                        const mtx::events::collections::TimelineEvents &timeline,
+                                        mtx::http::RequestErr err) {
+                                          if (err) {
+                                              nhlog::net()->error(
+                                                "Failed to retrieve event with id {}, which was "
+                                                "requested to show the replyTo for event {}",
+                                                relatedTo,
+                                                id);
+                                              return;
+                                          }
+                                          emit eventFetched(id, relatedTo, timeline);
+                                      });
+            return nullptr;
+        }
+        event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
+        events_by_id_.insert(index, event_ptr);
+    }
 
-        if (decrypt) {
-                if (auto encrypted =
-                      std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                        event_ptr)) {
-                        auto decrypted = decryptEvent(index, *encrypted);
-                        if (decrypted->event)
-                                return &*decrypted->event;
-                }
+    if (decrypt) {
+        if (auto encrypted =
+              std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
+            auto decrypted = decryptEvent(index, *encrypted);
+            if (decrypted->event)
+                return &*decrypted->event;
         }
+    }
 
-        return event_ptr;
+    return event_ptr;
 }
 
 olm::DecryptionErrorCode
 EventStore::decryptionError(std::string id)
 {
-        if (this->thread() != QThread::currentThread())
-                nhlog::db()->warn("{} called from a different thread!", __func__);
+    if (this->thread() != QThread::currentThread())
+        nhlog::db()->warn("{} called from a different thread!", __func__);
 
-        if (id.empty())
-                return olm::DecryptionErrorCode::NoError;
-
-        IdIndex index{room_id_, std::move(id)};
-        auto edits_ = edits(index.id);
-        if (!edits_.empty()) {
-                index.id = mtx::accessors::event_id(edits_.back());
-                auto event_ptr =
-                  new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
-                events_by_id_.insert(index, event_ptr);
-        }
+    if (id.empty())
+        return olm::DecryptionErrorCode::NoError;
 
-        auto event_ptr = events_by_id_.object(index);
-        if (!event_ptr) {
-                auto event = cache::client()->getEvent(room_id_, index.id);
-                if (!event) {
-                        return olm::DecryptionErrorCode::NoError;
-                }
-                event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
-                events_by_id_.insert(index, event_ptr);
+    IdIndex index{room_id_, std::move(id)};
+    auto edits_ = edits(index.id);
+    if (!edits_.empty()) {
+        index.id       = mtx::accessors::event_id(edits_.back());
+        auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
+        events_by_id_.insert(index, event_ptr);
+    }
+
+    auto event_ptr = events_by_id_.object(index);
+    if (!event_ptr) {
+        auto event = cache::client()->getEvent(room_id_, index.id);
+        if (!event) {
+            return olm::DecryptionErrorCode::NoError;
         }
+        event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
+        events_by_id_.insert(index, event_ptr);
+    }
 
-        if (auto encrypted =
-              std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
-                auto decrypted = decryptEvent(index, *encrypted);
-                return decrypted->error;
-        }
+    if (auto encrypted =
+          std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
+        auto decrypted = decryptEvent(index, *encrypted);
+        return decrypted->error;
+    }
 
-        return olm::DecryptionErrorCode::NoError;
+    return olm::DecryptionErrorCode::NoError;
 }
 
 void
 EventStore::fetchMore()
 {
-        if (noMoreMessages)
-                return;
-
-        mtx::http::MessagesOpts opts;
-        opts.room_id = room_id_;
-        opts.from    = cache::client()->previousBatchToken(room_id_);
-
-        nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from);
-
-        http::client()->messages(
-          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
-                  if (cache::client()->previousBatchToken(room_id_) != opts.from) {
-                          nhlog::net()->warn("Cache cleared while fetching more messages, dropping "
-                                             "/messages response");
-                          if (!opts.to.empty())
-                                  emit fetchedMore();
-                          return;
-                  }
-                  if (err) {
-                          nhlog::net()->error("failed to call /messages ({}): {} - {} - {}",
-                                              opts.room_id,
-                                              mtx::errors::to_string(err->matrix_error.errcode),
-                                              err->matrix_error.error,
-                                              err->parse_error);
-                          emit fetchedMore();
-                          return;
-                  }
-
-                  emit oldMessagesRetrieved(std::move(res));
-          });
+    if (noMoreMessages) {
+        emit fetchedMore();
+        return;
+    }
+
+    mtx::http::MessagesOpts opts;
+    opts.room_id = room_id_;
+    opts.from    = cache::client()->previousBatchToken(room_id_);
+
+    nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from);
+
+    http::client()->messages(
+      opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
+          if (cache::client()->previousBatchToken(room_id_) != opts.from) {
+              nhlog::net()->warn("Cache cleared while fetching more messages, dropping "
+                                 "/messages response");
+              if (!opts.to.empty())
+                  emit fetchedMore();
+              return;
+          }
+          if (err) {
+              nhlog::net()->error("failed to call /messages ({}): {} - {} - {}",
+                                  opts.room_id,
+                                  mtx::errors::to_string(err->matrix_error.errcode),
+                                  err->matrix_error.error,
+                                  err->parse_error);
+              emit fetchedMore();
+              return;
+          }
+
+          emit oldMessagesRetrieved(std::move(res));
+      });
 }
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index 59c1c7c077aa23727d2bb67a139a5c896b092d63..9b857dcfacf5214ed971261fd31479e91e5417f1 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -15,138 +15,136 @@
 #include <mtx/responses/messages.hpp>
 #include <mtx/responses/sync.hpp>
 
-#include "Olm.h"
 #include "Reaction.h"
+#include "encryption/Olm.h"
 
 class EventStore : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        EventStore(std::string room_id, QObject *parent);
-
-        // taken from QtPrivate::QHashCombine
-        static uint hashCombine(uint hash, uint seed)
-        {
-                return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2));
-        };
-        struct Index
+    EventStore(std::string room_id, QObject *parent);
+
+    // taken from QtPrivate::QHashCombine
+    static uint hashCombine(uint hash, uint seed)
+    {
+        return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2));
+    };
+    struct Index
+    {
+        std::string room;
+        uint64_t idx;
+
+        friend uint qHash(const Index &i, uint seed = 0) noexcept
         {
-                std::string room;
-                uint64_t idx;
-
-                friend uint qHash(const Index &i, uint seed = 0) noexcept
-                {
-                        seed =
-                          hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed);
-                        seed = hashCombine(qHash(i.idx, seed), seed);
-                        return seed;
-                }
-
-                friend bool operator==(const Index &a, const Index &b) noexcept
-                {
-                        return a.idx == b.idx && a.room == b.room;
-                }
-        };
-        struct IdIndex
+            seed = hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed);
+            seed = hashCombine(qHash(i.idx, seed), seed);
+            return seed;
+        }
+
+        friend bool operator==(const Index &a, const Index &b) noexcept
         {
-                std::string room, id;
-
-                friend uint qHash(const IdIndex &i, uint seed = 0) noexcept
-                {
-                        seed =
-                          hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed);
-                        seed = hashCombine(qHashBits(i.id.data(), (int)i.id.size(), seed), seed);
-                        return seed;
-                }
-
-                friend bool operator==(const IdIndex &a, const IdIndex &b) noexcept
-                {
-                        return a.id == b.id && a.room == b.room;
-                }
-        };
-
-        void fetchMore();
-        void handleSync(const mtx::responses::Timeline &events);
-
-        // optionally returns the event or nullptr and fetches it, after which it emits a
-        // relatedFetched event
-        mtx::events::collections::TimelineEvents *get(std::string id,
-                                                      std::string_view related_to,
-                                                      bool decrypt       = true,
-                                                      bool resolve_edits = true);
-        // always returns a proper event as long as the idx is valid
-        mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
-
-        QVariantList reactions(const std::string &event_id);
-        olm::DecryptionErrorCode decryptionError(std::string id);
-        void requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
-                            bool manual);
-
-        int size() const
+            return a.idx == b.idx && a.room == b.room;
+        }
+    };
+    struct IdIndex
+    {
+        std::string room, id;
+
+        friend uint qHash(const IdIndex &i, uint seed = 0) noexcept
         {
-                return (last != std::numeric_limits<uint64_t>::max() && last >= first)
-                         ? static_cast<int>(last - first) + 1
-                         : 0;
+            seed = hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed);
+            seed = hashCombine(qHashBits(i.id.data(), (int)i.id.size(), seed), seed);
+            return seed;
         }
-        int toExternalIdx(uint64_t idx) const { return static_cast<int>(idx - first); }
-        uint64_t toInternalIdx(int idx) const { return first + idx; }
 
-        std::optional<int> idToIndex(std::string_view id) const;
-        std::optional<std::string> indexToId(int idx) const;
+        friend bool operator==(const IdIndex &a, const IdIndex &b) noexcept
+        {
+            return a.id == b.id && a.room == b.room;
+        }
+    };
+
+    void fetchMore();
+    void handleSync(const mtx::responses::Timeline &events);
+
+    // optionally returns the event or nullptr and fetches it, after which it emits a
+    // relatedFetched event
+    mtx::events::collections::TimelineEvents *get(std::string id,
+                                                  std::string_view related_to,
+                                                  bool decrypt       = true,
+                                                  bool resolve_edits = true);
+    // always returns a proper event as long as the idx is valid
+    mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
+
+    QVariantList reactions(const std::string &event_id);
+    olm::DecryptionErrorCode decryptionError(std::string id);
+    void requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
+                        bool manual);
+
+    int size() const
+    {
+        return (last != std::numeric_limits<uint64_t>::max() && last >= first)
+                 ? static_cast<int>(last - first) + 1
+                 : 0;
+    }
+    int toExternalIdx(uint64_t idx) const { return static_cast<int>(idx - first); }
+    uint64_t toInternalIdx(int idx) const { return first + idx; }
+
+    std::optional<int> idToIndex(std::string_view id) const;
+    std::optional<std::string> indexToId(int idx) const;
 
 signals:
-        void beginInsertRows(int from, int to);
-        void endInsertRows();
-        void beginResetModel();
-        void endResetModel();
-        void dataChanged(int from, int to);
-        void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
-        void eventFetched(std::string id,
-                          std::string relatedTo,
-                          mtx::events::collections::TimelineEvents timeline);
-        void oldMessagesRetrieved(const mtx::responses::Messages &);
-        void fetchedMore();
-
-        void processPending();
-        void messageSent(std::string txn_id, std::string event_id);
-        void messageFailed(std::string txn_id);
-        void startDMVerification(
-          const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg);
-        void updateFlowEventId(std::string event_id);
+    void beginInsertRows(int from, int to);
+    void endInsertRows();
+    void beginResetModel();
+    void endResetModel();
+    void dataChanged(int from, int to);
+    void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
+    void eventFetched(std::string id,
+                      std::string relatedTo,
+                      mtx::events::collections::TimelineEvents timeline);
+    void oldMessagesRetrieved(const mtx::responses::Messages &);
+    void fetchedMore();
+
+    void processPending();
+    void messageSent(std::string txn_id, std::string event_id);
+    void messageFailed(std::string txn_id);
+    void startDMVerification(
+      const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg);
+    void updateFlowEventId(std::string event_id);
 
 public slots:
-        void addPending(mtx::events::collections::TimelineEvents event);
-        void receivedSessionKey(const std::string &session_id);
-        void clearTimeline();
-        void enableKeyRequests(bool suppressKeyRequests_);
+    void addPending(mtx::events::collections::TimelineEvents event);
+    void receivedSessionKey(const std::string &session_id);
+    void clearTimeline();
+    void enableKeyRequests(bool suppressKeyRequests_);
 
 private:
-        std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
-        olm::DecryptionResult *decryptEvent(
-          const IdIndex &idx,
-          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
-        void handle_room_verification(mtx::events::collections::TimelineEvents event);
-
-        std::string room_id_;
-
-        uint64_t first = std::numeric_limits<uint64_t>::max(),
-                 last  = std::numeric_limits<uint64_t>::max();
-
-        static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_;
-        static QCache<Index, mtx::events::collections::TimelineEvents> events_;
-        static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
-
-        struct PendingKeyRequests
-        {
-                std::string request_id;
-                std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
-                qint64 requested_at;
-        };
-        std::map<std::string, PendingKeyRequests> pending_key_requests;
-
-        std::string current_txn;
-        int current_txn_error_count = 0;
-        bool noMoreMessages         = false;
-        bool suppressKeyRequests    = true;
+    std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
+    olm::DecryptionResult *decryptEvent(
+      const IdIndex &idx,
+      const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
+    void handle_room_verification(mtx::events::collections::TimelineEvents event);
+
+    std::string room_id_;
+
+    uint64_t first = std::numeric_limits<uint64_t>::max(),
+             last  = std::numeric_limits<uint64_t>::max();
+
+    static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_;
+    static QCache<Index, mtx::events::collections::TimelineEvents> events_;
+    static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
+
+    struct PendingKeyRequests
+    {
+        std::string request_id;
+        std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
+        qint64 requested_at;
+    };
+    std::map<std::string, PendingKeyRequests> pending_key_requests;
+
+    std::string current_txn;
+    int current_txn_error_count = 0;
+    bool noMoreMessages         = false;
+    bool suppressKeyRequests    = true;
 };
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index f17081e57e8092797bb9a27634d4480ab966b27c..44df341198a699feee61cd21f48b520e26c52385 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -11,6 +11,7 @@
 #include <QMimeData>
 #include <QMimeDatabase>
 #include <QStandardPaths>
+#include <QTextBoundaryFinder>
 #include <QUrl>
 
 #include <QRegularExpression>
@@ -25,7 +26,6 @@
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
-#include "Olm.h"
 #include "RoomsModel.h"
 #include "TimelineModel.h"
 #include "TimelineViewManager.h"
@@ -42,342 +42,373 @@ static constexpr size_t INPUT_HISTORY_SIZE = 10;
 void
 InputBar::paste(bool fromMouse)
 {
-        const QMimeData *md = nullptr;
+    const QMimeData *md = nullptr;
 
-        if (fromMouse) {
-                if (QGuiApplication::clipboard()->supportsSelection()) {
-                        md = QGuiApplication::clipboard()->mimeData(QClipboard::Selection);
-                }
-        } else {
-                md = QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard);
-        }
+    if (fromMouse && QGuiApplication::clipboard()->supportsSelection()) {
+        md = QGuiApplication::clipboard()->mimeData(QClipboard::Selection);
+    } else {
+        md = QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard);
+    }
 
-        if (md)
-                insertMimeData(md);
+    if (md)
+        insertMimeData(md);
 }
 
 void
 InputBar::insertMimeData(const QMimeData *md)
 {
-        if (!md)
-                return;
-
-        nhlog::ui()->debug("Got mime formats: {}", md->formats().join(", ").toStdString());
-        const auto formats = md->formats().filter("/");
-        const auto image   = formats.filter("image/", Qt::CaseInsensitive);
-        const auto audio   = formats.filter("audio/", Qt::CaseInsensitive);
-        const auto video   = formats.filter("video/", Qt::CaseInsensitive);
-
-        if (!image.empty() && md->hasImage()) {
-                showPreview(*md, "", image);
-        } else if (!audio.empty()) {
-                showPreview(*md, "", audio);
-        } else if (!video.empty()) {
-                showPreview(*md, "", video);
-        } else if (md->hasUrls()) {
-                // Generic file path for any platform.
-                QString path;
-                for (auto &&u : md->urls()) {
-                        if (u.isLocalFile()) {
-                                path = u.toLocalFile();
-                                break;
-                        }
-                }
+    if (!md)
+        return;
+
+    nhlog::ui()->debug("Got mime formats: {}", md->formats().join(", ").toStdString());
+    const auto formats = md->formats().filter("/");
+    const auto image   = formats.filter("image/", Qt::CaseInsensitive);
+    const auto audio   = formats.filter("audio/", Qt::CaseInsensitive);
+    const auto video   = formats.filter("video/", Qt::CaseInsensitive);
+
+    if (md->hasImage()) {
+        if (formats.contains("image/svg+xml", Qt::CaseInsensitive)) {
+            showPreview(*md, "", QStringList("image/svg+xml"));
+        } else {
+            showPreview(*md, "", image);
+        }
+    } else if (!audio.empty()) {
+        showPreview(*md, "", audio);
+    } else if (!video.empty()) {
+        showPreview(*md, "", video);
+    } else if (md->hasUrls()) {
+        // Generic file path for any platform.
+        QString path;
+        for (auto &&u : md->urls()) {
+            if (u.isLocalFile()) {
+                path = u.toLocalFile();
+                break;
+            }
+        }
 
-                if (!path.isEmpty() && QFileInfo{path}.exists()) {
-                        showPreview(*md, path, formats);
-                } else {
-                        nhlog::ui()->warn("Clipboard does not contain any valid file paths.");
-                }
-        } else if (md->hasFormat("x-special/gnome-copied-files")) {
-                // Special case for X11 users. See "Notes for X11 Users" in md.
-                // Source: http://doc.qt.io/qt-5/qclipboard.html
-
-                // This MIME type returns a string with multiple lines separated by '\n'. The first
-                // line is the command to perform with the clipboard (not useful to us). The
-                // following lines are the file URIs.
-                //
-                // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function
-                // nautilus_clipboard_get_uri_list_from_selection_data()
-                // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c
-
-                auto data = md->data("x-special/gnome-copied-files").split('\n');
-                if (data.size() < 2) {
-                        nhlog::ui()->warn("MIME format is malformed, cannot perform paste.");
-                        return;
-                }
+        if (!path.isEmpty() && QFileInfo{path}.exists()) {
+            showPreview(*md, path, formats);
+        } else {
+            nhlog::ui()->warn("Clipboard does not contain any valid file paths.");
+        }
+    } else if (md->hasFormat("x-special/gnome-copied-files")) {
+        // Special case for X11 users. See "Notes for X11 Users" in md.
+        // Source: http://doc.qt.io/qt-5/qclipboard.html
+
+        // This MIME type returns a string with multiple lines separated by '\n'. The first
+        // line is the command to perform with the clipboard (not useful to us). The
+        // following lines are the file URIs.
+        //
+        // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function
+        // nautilus_clipboard_get_uri_list_from_selection_data()
+        // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c
+
+        auto data = md->data("x-special/gnome-copied-files").split('\n');
+        if (data.size() < 2) {
+            nhlog::ui()->warn("MIME format is malformed, cannot perform paste.");
+            return;
+        }
 
-                QString path;
-                for (int i = 1; i < data.size(); ++i) {
-                        QUrl url{data[i]};
-                        if (url.isLocalFile()) {
-                                path = url.toLocalFile();
-                                break;
-                        }
-                }
+        QString path;
+        for (int i = 1; i < data.size(); ++i) {
+            QUrl url{data[i]};
+            if (url.isLocalFile()) {
+                path = url.toLocalFile();
+                break;
+            }
+        }
 
-                if (!path.isEmpty()) {
-                        showPreview(*md, path, formats);
-                } else {
-                        nhlog::ui()->warn("Clipboard does not contain any valid file paths: {}",
-                                          data.join(", ").toStdString());
-                }
-        } else if (md->hasText()) {
-                emit insertText(md->text());
+        if (!path.isEmpty()) {
+            showPreview(*md, path, formats);
         } else {
-                nhlog::ui()->debug("formats: {}", md->formats().join(", ").toStdString());
+            nhlog::ui()->warn("Clipboard does not contain any valid file paths: {}",
+                              data.join(", ").toStdString());
         }
+    } else if (md->hasText()) {
+        emit insertText(md->text());
+    } else {
+        nhlog::ui()->debug("formats: {}", md->formats().join(", ").toStdString());
+    }
+}
+
+void
+InputBar::updateAtRoom(const QString &t)
+{
+    bool roomMention = false;
+
+    if (t.size() > 4) {
+        QTextBoundaryFinder finder(QTextBoundaryFinder::BoundaryType::Word, t);
+
+        finder.toStart();
+        do {
+            auto start = finder.position();
+            finder.toNextBoundary();
+            auto end = finder.position();
+            if (start > 0 && end - start >= 4 && t.midRef(start, end - start) == "room" &&
+                t.at(start - 1) == QChar('@')) {
+                roomMention = true;
+                break;
+            }
+        } while (finder.position() < t.size());
+    }
+
+    if (roomMention != this->containsAtRoom_) {
+        this->containsAtRoom_ = roomMention;
+        emit containsAtRoomChanged();
+    }
 }
 
 void
 InputBar::setText(QString newText)
 {
-        if (history_.empty())
-                history_.push_front(newText);
-        else
-                history_.front() = newText;
-        history_index_ = 0;
+    if (history_.empty())
+        history_.push_front(newText);
+    else
+        history_.front() = newText;
+    history_index_ = 0;
 
-        if (history_.size() == INPUT_HISTORY_SIZE)
-                history_.pop_back();
+    if (history_.size() == INPUT_HISTORY_SIZE)
+        history_.pop_back();
 
-        emit textChanged(newText);
+    updateAtRoom("");
+    emit textChanged(newText);
 }
 void
 InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_)
 {
-        if (text_.isEmpty())
-                stopTyping();
+    if (text_.isEmpty())
+        stopTyping();
+    else
+        startTyping();
+
+    if (text_ != text()) {
+        if (history_.empty())
+            history_.push_front(text_);
         else
-                startTyping();
+            history_.front() = text_;
+        history_index_ = 0;
 
-        if (text_ != text()) {
-                if (history_.empty())
-                        history_.push_front(text_);
-                else
-                        history_.front() = text_;
-                history_index_ = 0;
-        }
+        updateAtRoom(text_);
+    }
 
-        selectionStart = selectionStart_;
-        selectionEnd   = selectionEnd_;
-        cursorPosition = cursorPosition_;
+    selectionStart = selectionStart_;
+    selectionEnd   = selectionEnd_;
+    cursorPosition = cursorPosition_;
 }
 
 QString
 InputBar::text() const
 {
-        if (history_index_ < history_.size())
-                return history_.at(history_index_);
+    if (history_index_ < history_.size())
+        return history_.at(history_index_);
 
-        return "";
+    return "";
 }
 
 QString
 InputBar::previousText()
 {
-        history_index_++;
-        if (history_index_ >= INPUT_HISTORY_SIZE)
-                history_index_ = INPUT_HISTORY_SIZE;
-        else if (text().isEmpty())
-                history_index_--;
+    history_index_++;
+    if (history_index_ >= INPUT_HISTORY_SIZE)
+        history_index_ = INPUT_HISTORY_SIZE;
+    else if (text().isEmpty())
+        history_index_--;
 
-        return text();
+    updateAtRoom(text());
+    return text();
 }
 
 QString
 InputBar::nextText()
 {
-        history_index_--;
-        if (history_index_ >= INPUT_HISTORY_SIZE)
-                history_index_ = 0;
+    history_index_--;
+    if (history_index_ >= INPUT_HISTORY_SIZE)
+        history_index_ = 0;
 
-        return text();
+    updateAtRoom(text());
+    return text();
 }
 
 void
 InputBar::send()
 {
-        if (text().trimmed().isEmpty())
-                return;
-
-        nhlog::ui()->debug("Send: {}", text().toStdString());
-
-        auto wasEdit = !room->edit().isEmpty();
-
-        if (text().startsWith('/')) {
-                int command_end = text().indexOf(QRegularExpression("\\s"));
-                if (command_end == -1)
-                        command_end = text().size();
-                auto name = text().mid(1, command_end - 1);
-                auto args = text().mid(command_end + 1);
-                if (name.isEmpty() || name == "/") {
-                        message(args);
-                } else {
-                        command(name, args);
-                }
-        } else {
-                message(text());
-        }
+    if (text().trimmed().isEmpty())
+        return;
+
+    nhlog::ui()->debug("Send: {}", text().toStdString());
 
-        if (!wasEdit) {
-                history_.push_front("");
-                setText("");
+    auto wasEdit = !room->edit().isEmpty();
+
+    if (text().startsWith('/')) {
+        int command_end = text().indexOf(QRegularExpression("\\s"));
+        if (command_end == -1)
+            command_end = text().size();
+        auto name = text().mid(1, command_end - 1);
+        auto args = text().mid(command_end + 1);
+        if (name.isEmpty() || name == "/") {
+            message(args);
+        } else {
+            command(name, args);
         }
+    } else {
+        message(text());
+    }
+
+    if (!wasEdit) {
+        history_.push_front("");
+        setText("");
+    }
 }
 
 void
 InputBar::openFileSelection()
 {
-        const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
-        const auto fileName      = QFileDialog::getOpenFileName(
-          ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
+    const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
+    const auto fileName      = QFileDialog::getOpenFileName(
+      ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
 
-        if (fileName.isEmpty())
-                return;
+    if (fileName.isEmpty())
+        return;
 
-        QMimeDatabase db;
-        QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
+    QMimeDatabase db;
+    QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
 
-        QFile file{fileName};
+    QFile file{fileName};
 
-        if (!file.open(QIODevice::ReadOnly)) {
-                emit ChatPage::instance()->showNotification(
-                  QString("Error while reading media: %1").arg(file.errorString()));
-                return;
-        }
+    if (!file.open(QIODevice::ReadOnly)) {
+        emit ChatPage::instance()->showNotification(
+          QString("Error while reading media: %1").arg(file.errorString()));
+        return;
+    }
 
-        setUploading(true);
+    setUploading(true);
 
-        auto bin = file.readAll();
+    auto bin = file.readAll();
 
-        QMimeData data;
-        data.setData(mime.name(), bin);
+    QMimeData data;
+    data.setData(mime.name(), bin);
 
-        showPreview(data, fileName, QStringList{mime.name()});
+    showPreview(data, fileName, QStringList{mime.name()});
 }
 
 void
 InputBar::message(QString msg, MarkdownOverride useMarkdown, bool rainbowify)
 {
-        mtx::events::msg::Text text = {};
-        text.body                   = msg.trimmed().toStdString();
+    mtx::events::msg::Text text = {};
+    text.body                   = msg.trimmed().toStdString();
+
+    if ((ChatPage::instance()->userSettings()->markdown() &&
+         useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
+        useMarkdown == MarkdownOverride::ON) {
+        text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString();
+        // Remove markdown links by completer
+        text.body = msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
+
+        // Don't send formatted_body, when we don't need to
+        if (text.formatted_body.find("<") == std::string::npos)
+            text.formatted_body = "";
+        else
+            text.format = "org.matrix.custom.html";
+    }
 
-        if ((ChatPage::instance()->userSettings()->markdown() &&
-             useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
-            useMarkdown == MarkdownOverride::ON) {
-                text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString();
-                // Remove markdown links by completer
-                text.body =
-                  msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
-
-                // Don't send formatted_body, when we don't need to
-                if (text.formatted_body.find("<") == std::string::npos)
-                        text.formatted_body = "";
-                else
-                        text.format = "org.matrix.custom.html";
+    if (!room->edit().isEmpty()) {
+        if (!room->reply().isEmpty()) {
+            text.relations.relations.push_back(
+              {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
         }
 
-        if (!room->edit().isEmpty()) {
-                if (!room->reply().isEmpty()) {
-                        text.relations.relations.push_back(
-                          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-                }
-
-                text.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-
-        } else if (!room->reply().isEmpty()) {
-                auto related = room->relatedInfo(room->reply());
-
-                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").arg(line);
-                        }
-                }
+        text.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+
+    } else if (!room->reply().isEmpty()) {
+        auto related = room->relatedInfo(room->reply());
+
+        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").arg(line);
+            }
+        }
 
-                text.body = QString("%1\n%2").arg(body).arg(msg).toStdString();
+        text.body = QString("%1\n%2").arg(body).arg(msg).toStdString();
 
-                // NOTE(Nico): rich replies always need a formatted_body!
-                text.format = "org.matrix.custom.html";
-                if ((ChatPage::instance()->userSettings()->markdown() &&
-                     useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
-                    useMarkdown == MarkdownOverride::ON)
-                        text.formatted_body = utils::getFormattedQuoteBody(
-                                                related, utils::markdownToHtml(msg, rainbowify))
-                                                .toStdString();
-                else
-                        text.formatted_body =
-                          utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString();
+        // NOTE(Nico): rich replies always need a formatted_body!
+        text.format = "org.matrix.custom.html";
+        if ((ChatPage::instance()->userSettings()->markdown() &&
+             useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
+            useMarkdown == MarkdownOverride::ON)
+            text.formatted_body =
+              utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg, rainbowify))
+                .toStdString();
+        else
+            text.formatted_body =
+              utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString();
 
-                text.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, related.related_event});
-        }
+        text.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, related.related_event});
+    }
 
-        room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
+    room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
 }
 
 void
 InputBar::emote(QString msg, bool rainbowify)
 {
-        auto html = utils::markdownToHtml(msg, rainbowify);
-
-        mtx::events::msg::Emote emote;
-        emote.body = msg.trimmed().toStdString();
-
-        if (html != msg.trimmed().toHtmlEscaped() &&
-            ChatPage::instance()->userSettings()->markdown()) {
-                emote.formatted_body = html.toStdString();
-                emote.format         = "org.matrix.custom.html";
-                // Remove markdown links by completer
-                emote.body =
-                  msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
-        }
-
-        if (!room->reply().isEmpty()) {
-                emote.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-        }
-        if (!room->edit().isEmpty()) {
-                emote.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-        }
-
-        room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
+    auto html = utils::markdownToHtml(msg, rainbowify);
+
+    mtx::events::msg::Emote emote;
+    emote.body = msg.trimmed().toStdString();
+
+    if (html != msg.trimmed().toHtmlEscaped() && ChatPage::instance()->userSettings()->markdown()) {
+        emote.formatted_body = html.toStdString();
+        emote.format         = "org.matrix.custom.html";
+        // Remove markdown links by completer
+        emote.body =
+          msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
+    }
+
+    if (!room->reply().isEmpty()) {
+        emote.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+    }
+    if (!room->edit().isEmpty()) {
+        emote.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+    }
+
+    room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
 }
 
 void
 InputBar::notice(QString msg, bool rainbowify)
 {
-        auto html = utils::markdownToHtml(msg, rainbowify);
-
-        mtx::events::msg::Notice notice;
-        notice.body = msg.trimmed().toStdString();
-
-        if (html != msg.trimmed().toHtmlEscaped() &&
-            ChatPage::instance()->userSettings()->markdown()) {
-                notice.formatted_body = html.toStdString();
-                notice.format         = "org.matrix.custom.html";
-                // Remove markdown links by completer
-                notice.body =
-                  msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
-        }
-
-        if (!room->reply().isEmpty()) {
-                notice.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-        }
-        if (!room->edit().isEmpty()) {
-                notice.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-        }
-
-        room->sendMessageEvent(notice, mtx::events::EventType::RoomMessage);
+    auto html = utils::markdownToHtml(msg, rainbowify);
+
+    mtx::events::msg::Notice notice;
+    notice.body = msg.trimmed().toStdString();
+
+    if (html != msg.trimmed().toHtmlEscaped() && ChatPage::instance()->userSettings()->markdown()) {
+        notice.formatted_body = html.toStdString();
+        notice.format         = "org.matrix.custom.html";
+        // Remove markdown links by completer
+        notice.body =
+          msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
+    }
+
+    if (!room->reply().isEmpty()) {
+        notice.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+    }
+    if (!room->edit().isEmpty()) {
+        notice.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+    }
+
+    room->sendMessageEvent(notice, mtx::events::EventType::RoomMessage);
 }
 
 void
@@ -389,29 +420,29 @@ InputBar::image(const QString &filename,
                 const QSize &dimensions,
                 const QString &blurhash)
 {
-        mtx::events::msg::Image image;
-        image.info.mimetype = mime.toStdString();
-        image.info.size     = dsize;
-        image.info.blurhash = blurhash.toStdString();
-        image.body          = filename.toStdString();
-        image.info.h        = dimensions.height();
-        image.info.w        = dimensions.width();
-
-        if (file)
-                image.file = file;
-        else
-                image.url = url.toStdString();
-
-        if (!room->reply().isEmpty()) {
-                image.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-        }
-        if (!room->edit().isEmpty()) {
-                image.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-        }
-
-        room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
+    mtx::events::msg::Image image;
+    image.info.mimetype = mime.toStdString();
+    image.info.size     = dsize;
+    image.info.blurhash = blurhash.toStdString();
+    image.body          = filename.toStdString();
+    image.info.h        = dimensions.height();
+    image.info.w        = dimensions.width();
+
+    if (file)
+        image.file = file;
+    else
+        image.url = url.toStdString();
+
+    if (!room->reply().isEmpty()) {
+        image.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+    }
+    if (!room->edit().isEmpty()) {
+        image.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+    }
+
+    room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
 }
 
 void
@@ -421,26 +452,26 @@ InputBar::file(const QString &filename,
                const QString &mime,
                uint64_t dsize)
 {
-        mtx::events::msg::File file;
-        file.info.mimetype = mime.toStdString();
-        file.info.size     = dsize;
-        file.body          = filename.toStdString();
-
-        if (encryptedFile)
-                file.file = encryptedFile;
-        else
-                file.url = url.toStdString();
-
-        if (!room->reply().isEmpty()) {
-                file.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-        }
-        if (!room->edit().isEmpty()) {
-                file.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-        }
-
-        room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
+    mtx::events::msg::File file;
+    file.info.mimetype = mime.toStdString();
+    file.info.size     = dsize;
+    file.body          = filename.toStdString();
+
+    if (encryptedFile)
+        file.file = encryptedFile;
+    else
+        file.url = url.toStdString();
+
+    if (!room->reply().isEmpty()) {
+        file.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+    }
+    if (!room->edit().isEmpty()) {
+        file.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+    }
+
+    room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
 }
 
 void
@@ -450,27 +481,27 @@ InputBar::audio(const QString &filename,
                 const QString &mime,
                 uint64_t dsize)
 {
-        mtx::events::msg::Audio audio;
-        audio.info.mimetype = mime.toStdString();
-        audio.info.size     = dsize;
-        audio.body          = filename.toStdString();
-        audio.url           = url.toStdString();
-
-        if (file)
-                audio.file = file;
-        else
-                audio.url = url.toStdString();
-
-        if (!room->reply().isEmpty()) {
-                audio.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-        }
-        if (!room->edit().isEmpty()) {
-                audio.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-        }
-
-        room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
+    mtx::events::msg::Audio audio;
+    audio.info.mimetype = mime.toStdString();
+    audio.info.size     = dsize;
+    audio.body          = filename.toStdString();
+    audio.url           = url.toStdString();
+
+    if (file)
+        audio.file = file;
+    else
+        audio.url = url.toStdString();
+
+    if (!room->reply().isEmpty()) {
+        audio.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+    }
+    if (!room->edit().isEmpty()) {
+        audio.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+    }
+
+    room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
 }
 
 void
@@ -480,296 +511,323 @@ InputBar::video(const QString &filename,
                 const QString &mime,
                 uint64_t dsize)
 {
-        mtx::events::msg::Video video;
-        video.info.mimetype = mime.toStdString();
-        video.info.size     = dsize;
-        video.body          = filename.toStdString();
-
-        if (file)
-                video.file = file;
-        else
-                video.url = url.toStdString();
-
-        if (!room->reply().isEmpty()) {
-                video.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-        }
-        if (!room->edit().isEmpty()) {
-                video.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-        }
-
-        room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
+    mtx::events::msg::Video video;
+    video.info.mimetype = mime.toStdString();
+    video.info.size     = dsize;
+    video.body          = filename.toStdString();
+
+    if (file)
+        video.file = file;
+    else
+        video.url = url.toStdString();
+
+    if (!room->reply().isEmpty()) {
+        video.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+    }
+    if (!room->edit().isEmpty()) {
+        video.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+    }
+
+    room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
 }
 
 void
 InputBar::sticker(CombinedImagePackModel *model, int row)
 {
-        if (!model || row < 0)
-                return;
-
-        auto img = model->imageAt(row);
-
-        mtx::events::msg::StickerImage sticker{};
-        sticker.info = img.info.value_or(mtx::common::ImageInfo{});
-        sticker.url  = img.url;
-        sticker.body = img.body;
-
-        if (!room->reply().isEmpty()) {
-                sticker.relations.relations.push_back(
-                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
-        }
-        if (!room->edit().isEmpty()) {
-                sticker.relations.relations.push_back(
-                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
-        }
-
-        room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
+    if (!model || row < 0)
+        return;
+
+    auto img = model->imageAt(row);
+
+    mtx::events::msg::StickerImage sticker{};
+    sticker.info = img.info.value_or(mtx::common::ImageInfo{});
+    sticker.url  = img.url;
+    sticker.body = img.body;
+
+    // workaround for https://github.com/vector-im/element-ios/issues/2353
+    sticker.info.thumbnail_url           = sticker.url;
+    sticker.info.thumbnail_info.mimetype = sticker.info.mimetype;
+    sticker.info.thumbnail_info.size     = sticker.info.size;
+    sticker.info.thumbnail_info.h        = sticker.info.h;
+    sticker.info.thumbnail_info.w        = sticker.info.w;
+
+    if (!room->reply().isEmpty()) {
+        sticker.relations.relations.push_back(
+          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+    }
+    if (!room->edit().isEmpty()) {
+        sticker.relations.relations.push_back(
+          {mtx::common::RelationType::Replace, room->edit().toStdString()});
+    }
+
+    room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
 }
 
 void
 InputBar::command(QString command, QString args)
 {
-        if (command == "me") {
-                emote(args, false);
-        } else if (command == "react") {
-                auto eventId = room->reply();
-                if (!eventId.isEmpty())
-                        reaction(eventId, args.trimmed());
-        } else if (command == "join") {
-                ChatPage::instance()->joinRoom(args);
-        } else if (command == "part" || command == "leave") {
-                MainWindow::instance()->openLeaveRoomDialog(room->roomId());
-        } else if (command == "invite") {
-                ChatPage::instance()->inviteUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
-        } else if (command == "kick") {
-                ChatPage::instance()->kickUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
-        } else if (command == "ban") {
-                ChatPage::instance()->banUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
-        } else if (command == "unban") {
-                ChatPage::instance()->unbanUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
-        } else if (command == "roomnick") {
-                mtx::events::state::Member member;
-                member.display_name = args.toStdString();
-                member.avatar_url =
-                  cache::avatarUrl(room->roomId(),
-                                   QString::fromStdString(http::client()->user_id().to_string()))
-                    .toStdString();
-                member.membership = mtx::events::state::Membership::Join;
-
-                http::client()->send_state_event(
-                  room->roomId().toStdString(),
-                  http::client()->user_id().to_string(),
-                  member,
-                  [](mtx::responses::EventId, mtx::http::RequestErr err) {
-                          if (err)
-                                  nhlog::net()->error("Failed to set room displayname: {}",
-                                                      err->matrix_error.error);
-                  });
-        } else if (command == "shrug") {
-                message("¯\\_(ツ)_/¯" + (args.isEmpty() ? "" : " " + args));
-        } else if (command == "fliptable") {
-                message("(╯°□°)╯︵ ┻━┻");
-        } else if (command == "unfliptable") {
-                message(" ┯━┯╭( º _ º╭)");
-        } else if (command == "sovietflip") {
-                message("ノ┬─┬ノ ︵ ( \\o°o)\\");
-        } else if (command == "clear-timeline") {
-                room->clearTimeline();
-        } else if (command == "rotate-megolm-session") {
-                cache::dropOutboundMegolmSession(room->roomId().toStdString());
-        } else if (command == "md") {
-                message(args, MarkdownOverride::ON);
-        } else if (command == "plain") {
-                message(args, MarkdownOverride::OFF);
-        } else if (command == "rainbow") {
-                message(args, MarkdownOverride::ON, true);
-        } else if (command == "rainbowme") {
-                emote(args, true);
-        } else if (command == "notice") {
-                notice(args, false);
-        } else if (command == "rainbownotice") {
-                notice(args, true);
+    if (command == "me") {
+        emote(args, false);
+    } else if (command == "react") {
+        auto eventId = room->reply();
+        if (!eventId.isEmpty())
+            reaction(eventId, args.trimmed());
+    } else if (command == "join") {
+        ChatPage::instance()->joinRoom(args);
+    } else if (command == "part" || command == "leave") {
+        ChatPage::instance()->timelineManager()->openLeaveRoomDialog(room->roomId());
+    } else if (command == "invite") {
+        ChatPage::instance()->inviteUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
+    } else if (command == "kick") {
+        ChatPage::instance()->kickUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
+    } else if (command == "ban") {
+        ChatPage::instance()->banUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
+    } else if (command == "unban") {
+        ChatPage::instance()->unbanUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
+    } else if (command == "roomnick") {
+        mtx::events::state::Member member;
+        member.display_name = args.toStdString();
+        member.avatar_url =
+          cache::avatarUrl(room->roomId(),
+                           QString::fromStdString(http::client()->user_id().to_string()))
+            .toStdString();
+        member.membership = mtx::events::state::Membership::Join;
+
+        http::client()->send_state_event(room->roomId().toStdString(),
+                                         http::client()->user_id().to_string(),
+                                         member,
+                                         [](mtx::responses::EventId, mtx::http::RequestErr err) {
+                                             if (err)
+                                                 nhlog::net()->error(
+                                                   "Failed to set room displayname: {}",
+                                                   err->matrix_error.error);
+                                         });
+    } else if (command == "shrug") {
+        message("¯\\_(ツ)_/¯" + (args.isEmpty() ? "" : " " + args));
+    } else if (command == "fliptable") {
+        message("(╯°□°)╯︵ ┻━┻");
+    } else if (command == "unfliptable") {
+        message(" ┯━┯╭( º _ º╭)");
+    } else if (command == "sovietflip") {
+        message("ノ┬─┬ノ ︵ ( \\o°o)\\");
+    } else if (command == "clear-timeline") {
+        room->clearTimeline();
+    } else if (command == "rotate-megolm-session") {
+        cache::dropOutboundMegolmSession(room->roomId().toStdString());
+    } else if (command == "md") {
+        message(args, MarkdownOverride::ON);
+    } else if (command == "plain") {
+        message(args, MarkdownOverride::OFF);
+    } else if (command == "rainbow") {
+        message(args, MarkdownOverride::ON, true);
+    } else if (command == "rainbowme") {
+        emote(args, true);
+    } else if (command == "notice") {
+        notice(args, false);
+    } else if (command == "rainbownotice") {
+        notice(args, true);
+    } else if (command == "goto") {
+        // Goto has three different modes:
+        // 1 - Going directly to a given event ID
+        if (args[0] == '$') {
+            room->showEvent(args);
+            return;
+        }
+        // 2 - Going directly to a given message index
+        if (args[0] >= '0' && args[0] <= '9') {
+            room->showEvent(args);
+            return;
         }
+        // 3 - Matrix URI handler, as if you clicked the URI
+        if (ChatPage::instance()->handleMatrixUri(args)) {
+            return;
+        }
+        nhlog::net()->error("Could not resolve goto: {}", args.toStdString());
+    }
 }
 
 void
 InputBar::showPreview(const QMimeData &source, QString path, const QStringList &formats)
 {
-        dialogs::PreviewUploadOverlay *previewDialog_ =
-          new dialogs::PreviewUploadOverlay(ChatPage::instance());
-        previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
-
-        if (source.hasImage())
-                previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()),
-                                           formats.front());
-        else if (!path.isEmpty())
-                previewDialog_->setPreview(path);
-        else if (!formats.isEmpty()) {
-                auto mime = formats.first();
-                previewDialog_->setPreview(source.data(mime), mime);
+    dialogs::PreviewUploadOverlay *previewDialog_ =
+      new dialogs::PreviewUploadOverlay(ChatPage::instance());
+    previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
+
+    // Force SVG to _not_ be handled as an image, but as raw data
+    if (source.hasImage() && (!formats.size() || formats.front() != "image/svg+xml")) {
+        if (formats.size() && formats.front().startsWith("image/")) {
+            // known format, keep as-is
+            previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()), formats.front());
         } else {
-                setUploading(false);
-                previewDialog_->deleteLater();
-                return;
+            // unknown image format, default to image/png
+            previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()), "image/png");
         }
-
-        connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() {
-                setUploading(false);
-        });
-
-        connect(
-          previewDialog_,
-          &dialogs::PreviewUploadOverlay::confirmUpload,
-          this,
-          [this](const QByteArray data, const QString &mime, const QString &fn) {
-                  setUploading(true);
-
-                  setText("");
-
-                  auto payload = std::string(data.data(), data.size());
-                  std::optional<mtx::crypto::EncryptedFile> encryptedFile;
-                  if (cache::isRoomEncrypted(room->roomId().toStdString())) {
-                          mtx::crypto::BinaryBuf buf;
-                          std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
-                          payload                      = mtx::crypto::to_string(buf);
+    } else if (!path.isEmpty())
+        previewDialog_->setPreview(path);
+    else if (!formats.isEmpty()) {
+        auto mime = formats.first();
+        previewDialog_->setPreview(source.data(mime), mime);
+    } else {
+        setUploading(false);
+        previewDialog_->deleteLater();
+        return;
+    }
+
+    connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() {
+        setUploading(false);
+    });
+
+    connect(
+      previewDialog_,
+      &dialogs::PreviewUploadOverlay::confirmUpload,
+      this,
+      [this](const QByteArray data, const QString &mime, const QString &fn) {
+          if (!data.size()) {
+              nhlog::ui()->warn("Attempted to upload zero-byte file?! Mimetype {}, filename {}",
+                                mime.toStdString(),
+                                fn.toStdString());
+              return;
+          }
+          setUploading(true);
+
+          setText("");
+
+          auto payload = std::string(data.data(), data.size());
+          std::optional<mtx::crypto::EncryptedFile> encryptedFile;
+          if (cache::isRoomEncrypted(room->roomId().toStdString())) {
+              mtx::crypto::BinaryBuf buf;
+              std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
+              payload                      = mtx::crypto::to_string(buf);
+          }
+
+          QSize dimensions;
+          QString blurhash;
+          auto mimeClass = mime.split("/")[0];
+          nhlog::ui()->debug("Mime: {}", mime.toStdString());
+          if (mimeClass == "image") {
+              QImage img = utils::readImage(data);
+
+              dimensions = img.size();
+              if (img.height() > 200 && img.width() > 360)
+                  img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding);
+              std::vector<unsigned char> data_;
+              for (int y = 0; y < img.height(); y++) {
+                  for (int x = 0; x < img.width(); x++) {
+                      auto p = img.pixel(x, y);
+                      data_.push_back(static_cast<unsigned char>(qRed(p)));
+                      data_.push_back(static_cast<unsigned char>(qGreen(p)));
+                      data_.push_back(static_cast<unsigned char>(qBlue(p)));
                   }
+              }
+              blurhash = QString::fromStdString(
+                blurhash::encode(data_.data(), img.width(), img.height(), 4, 3));
+          }
+
+          http::client()->upload(
+            payload,
+            encryptedFile ? "application/octet-stream" : mime.toStdString(),
+            QFileInfo(fn).fileName().toStdString(),
+            [this,
+             filename      = fn,
+             encryptedFile = std::move(encryptedFile),
+             mimeClass,
+             mime,
+             size = payload.size(),
+             dimensions,
+             blurhash](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) mutable {
+                if (err) {
+                    emit ChatPage::instance()->showNotification(
+                      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));
+                    setUploading(false);
+                    return;
+                }
 
-                  QSize dimensions;
-                  QString blurhash;
-                  auto mimeClass = mime.split("/")[0];
-                  nhlog::ui()->debug("Mime: {}", mime.toStdString());
-                  if (mimeClass == "image") {
-                          QImage img = utils::readImage(data);
-
-                          dimensions = img.size();
-                          if (img.height() > 200 && img.width() > 360)
-                                  img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding);
-                          std::vector<unsigned char> data_;
-                          for (int y = 0; y < img.height(); y++) {
-                                  for (int x = 0; x < img.width(); x++) {
-                                          auto p = img.pixel(x, y);
-                                          data_.push_back(static_cast<unsigned char>(qRed(p)));
-                                          data_.push_back(static_cast<unsigned char>(qGreen(p)));
-                                          data_.push_back(static_cast<unsigned char>(qBlue(p)));
-                                  }
-                          }
-                          blurhash = QString::fromStdString(
-                            blurhash::encode(data_.data(), img.width(), img.height(), 4, 3));
-                  }
+                auto url = QString::fromStdString(res.content_uri);
+                if (encryptedFile)
+                    encryptedFile->url = res.content_uri;
 
-                  http::client()->upload(
-                    payload,
-                    encryptedFile ? "application/octet-stream" : mime.toStdString(),
-                    QFileInfo(fn).fileName().toStdString(),
-                    [this,
-                     filename      = fn,
-                     encryptedFile = std::move(encryptedFile),
-                     mimeClass,
-                     mime,
-                     size = payload.size(),
-                     dimensions,
-                     blurhash](const mtx::responses::ContentURI &res,
-                               mtx::http::RequestErr err) mutable {
-                            if (err) {
-                                    emit ChatPage::instance()->showNotification(
-                                      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));
-                                    setUploading(false);
-                                    return;
-                            }
-
-                            auto url = QString::fromStdString(res.content_uri);
-                            if (encryptedFile)
-                                    encryptedFile->url = res.content_uri;
-
-                            if (mimeClass == "image")
-                                    image(filename,
-                                          encryptedFile,
-                                          url,
-                                          mime,
-                                          size,
-                                          dimensions,
-                                          blurhash);
-                            else if (mimeClass == "audio")
-                                    audio(filename, encryptedFile, url, mime, size);
-                            else if (mimeClass == "video")
-                                    video(filename, encryptedFile, url, mime, size);
-                            else
-                                    file(filename, encryptedFile, url, mime, size);
-
-                            setUploading(false);
-                    });
-          });
+                if (mimeClass == "image")
+                    image(filename, encryptedFile, url, mime, size, dimensions, blurhash);
+                else if (mimeClass == "audio")
+                    audio(filename, encryptedFile, url, mime, size);
+                else if (mimeClass == "video")
+                    video(filename, encryptedFile, url, mime, size);
+                else
+                    file(filename, encryptedFile, url, mime, size);
+
+                setUploading(false);
+            });
+      });
 }
 
 void
 InputBar::startTyping()
 {
-        if (!typingRefresh_.isActive()) {
-                typingRefresh_.start();
-
-                if (ChatPage::instance()->userSettings()->typingNotifications()) {
-                        http::client()->start_typing(
-                          room->roomId().toStdString(), 10'000, [](mtx::http::RequestErr err) {
-                                  if (err) {
-                                          nhlog::net()->warn(
-                                            "failed to send typing notification: {}",
-                                            err->matrix_error.error);
-                                  }
-                          });
-                }
+    if (!typingRefresh_.isActive()) {
+        typingRefresh_.start();
+
+        if (ChatPage::instance()->userSettings()->typingNotifications()) {
+            http::client()->start_typing(
+              room->roomId().toStdString(), 10'000, [](mtx::http::RequestErr err) {
+                  if (err) {
+                      nhlog::net()->warn("failed to send typing notification: {}",
+                                         err->matrix_error.error);
+                  }
+              });
         }
-        typingTimeout_.start();
+    }
+    typingTimeout_.start();
 }
 void
 InputBar::stopTyping()
 {
-        typingRefresh_.stop();
-        typingTimeout_.stop();
+    typingRefresh_.stop();
+    typingTimeout_.stop();
 
-        if (!ChatPage::instance()->userSettings()->typingNotifications())
-                return;
+    if (!ChatPage::instance()->userSettings()->typingNotifications())
+        return;
 
-        http::client()->stop_typing(room->roomId().toStdString(), [](mtx::http::RequestErr err) {
-                if (err) {
-                        nhlog::net()->warn("failed to stop typing notifications: {}",
-                                           err->matrix_error.error);
-                }
-        });
+    http::client()->stop_typing(room->roomId().toStdString(), [](mtx::http::RequestErr err) {
+        if (err) {
+            nhlog::net()->warn("failed to stop typing notifications: {}", err->matrix_error.error);
+        }
+    });
 }
 
 void
 InputBar::reaction(const QString &reactedEvent, const QString &reactionKey)
 {
-        auto reactions = room->reactions(reactedEvent.toStdString());
-
-        QString selfReactedEvent;
-        for (const auto &reaction : reactions) {
-                if (reactionKey == reaction.key_) {
-                        selfReactedEvent = reaction.selfReactedEvent_;
-                        break;
-                }
-        }
+    auto reactions = room->reactions(reactedEvent.toStdString());
 
-        if (selfReactedEvent.startsWith("m"))
-                return;
-
-        // If selfReactedEvent is empty, that means we haven't previously reacted
-        if (selfReactedEvent.isEmpty()) {
-                mtx::events::msg::Reaction reaction;
-                mtx::common::Relation rel;
-                rel.rel_type = mtx::common::RelationType::Annotation;
-                rel.event_id = reactedEvent.toStdString();
-                rel.key      = reactionKey.toStdString();
-                reaction.relations.relations.push_back(rel);
-
-                room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
-                // Otherwise, we have previously reacted and the reaction should be redacted
-        } else {
-                room->redactEvent(selfReactedEvent);
+    QString selfReactedEvent;
+    for (const auto &reaction : reactions) {
+        if (reactionKey == reaction.key_) {
+            selfReactedEvent = reaction.selfReactedEvent_;
+            break;
         }
+    }
+
+    if (selfReactedEvent.startsWith("m"))
+        return;
+
+    // If selfReactedEvent is empty, that means we haven't previously reacted
+    if (selfReactedEvent.isEmpty()) {
+        mtx::events::msg::Reaction reaction;
+        mtx::common::Relation rel;
+        rel.rel_type = mtx::common::RelationType::Annotation;
+        rel.event_id = reactedEvent.toStdString();
+        rel.key      = reactionKey.toStdString();
+        reaction.relations.relations.push_back(rel);
+
+        room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
+        // Otherwise, we have previously reacted and the reaction should be redacted
+    } else {
+        room->redactEvent(selfReactedEvent);
+    }
 }
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 2e6fb5c06bfe4fb0b65e86bf63f72c047cf99822..4a0f440113b12e7947742b7cb2b0f066d7845c6c 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -19,97 +19,105 @@ class QStringList;
 
 enum class MarkdownOverride
 {
-        NOT_SPECIFIED, // no override set
-        ON,
-        OFF,
+    NOT_SPECIFIED, // no override set
+    ON,
+    OFF,
 };
 
 class InputBar : public QObject
 {
-        Q_OBJECT
-        Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
+    Q_OBJECT
+    Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
+    Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged)
+    Q_PROPERTY(QString text READ text NOTIFY textChanged)
 
 public:
-        InputBar(TimelineModel *parent)
-          : QObject()
-          , room(parent)
-        {
-                typingRefresh_.setInterval(10'000);
-                typingRefresh_.setSingleShot(true);
-                typingTimeout_.setInterval(5'000);
-                typingTimeout_.setSingleShot(true);
-                connect(&typingRefresh_, &QTimer::timeout, this, &InputBar::startTyping);
-                connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping);
-        }
+    InputBar(TimelineModel *parent)
+      : QObject()
+      , room(parent)
+    {
+        typingRefresh_.setInterval(10'000);
+        typingRefresh_.setSingleShot(true);
+        typingTimeout_.setInterval(5'000);
+        typingTimeout_.setSingleShot(true);
+        connect(&typingRefresh_, &QTimer::timeout, this, &InputBar::startTyping);
+        connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping);
+    }
 
 public slots:
-        QString text() const;
-        QString previousText();
-        QString nextText();
-        void setText(QString newText);
-
-        void send();
-        void paste(bool fromMouse);
-        void insertMimeData(const QMimeData *data);
-        void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
-        void openFileSelection();
-        bool uploading() const { return uploading_; }
-        void message(QString body,
-                     MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
-                     bool rainbowify              = false);
-        void reaction(const QString &reactedEvent, const QString &reactionKey);
-        void sticker(CombinedImagePackModel *model, int row);
+    QString text() const;
+    QString previousText();
+    QString nextText();
+    void setText(QString newText);
+
+    bool containsAtRoom() const { return containsAtRoom_; }
+
+    void send();
+    void paste(bool fromMouse);
+    void insertMimeData(const QMimeData *data);
+    void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
+    void openFileSelection();
+    bool uploading() const { return uploading_; }
+    void message(QString body,
+                 MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
+                 bool rainbowify              = false);
+    void reaction(const QString &reactedEvent, const QString &reactionKey);
+    void sticker(CombinedImagePackModel *model, int row);
 
 private slots:
-        void startTyping();
-        void stopTyping();
+    void startTyping();
+    void stopTyping();
 
 signals:
-        void insertText(QString text);
-        void textChanged(QString newText);
-        void uploadingChanged(bool value);
+    void insertText(QString text);
+    void textChanged(QString newText);
+    void uploadingChanged(bool value);
+    void containsAtRoomChanged();
 
 private:
-        void emote(QString body, bool rainbowify);
-        void notice(QString body, bool rainbowify);
-        void command(QString name, QString args);
-        void image(const QString &filename,
-                   const std::optional<mtx::crypto::EncryptedFile> &file,
-                   const QString &url,
-                   const QString &mime,
-                   uint64_t dsize,
-                   const QSize &dimensions,
-                   const QString &blurhash);
-        void file(const QString &filename,
-                  const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
-                  const QString &url,
-                  const QString &mime,
-                  uint64_t dsize);
-        void audio(const QString &filename,
-                   const std::optional<mtx::crypto::EncryptedFile> &file,
-                   const QString &url,
-                   const QString &mime,
-                   uint64_t dsize);
-        void video(const QString &filename,
-                   const std::optional<mtx::crypto::EncryptedFile> &file,
-                   const QString &url,
-                   const QString &mime,
-                   uint64_t dsize);
-
-        void showPreview(const QMimeData &source, QString path, const QStringList &formats);
-        void setUploading(bool value)
-        {
-                if (value != uploading_) {
-                        uploading_ = value;
-                        emit uploadingChanged(value);
-                }
+    void emote(QString body, bool rainbowify);
+    void notice(QString body, bool rainbowify);
+    void command(QString name, QString args);
+    void image(const QString &filename,
+               const std::optional<mtx::crypto::EncryptedFile> &file,
+               const QString &url,
+               const QString &mime,
+               uint64_t dsize,
+               const QSize &dimensions,
+               const QString &blurhash);
+    void file(const QString &filename,
+              const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
+              const QString &url,
+              const QString &mime,
+              uint64_t dsize);
+    void audio(const QString &filename,
+               const std::optional<mtx::crypto::EncryptedFile> &file,
+               const QString &url,
+               const QString &mime,
+               uint64_t dsize);
+    void video(const QString &filename,
+               const std::optional<mtx::crypto::EncryptedFile> &file,
+               const QString &url,
+               const QString &mime,
+               uint64_t dsize);
+
+    void showPreview(const QMimeData &source, QString path, const QStringList &formats);
+    void setUploading(bool value)
+    {
+        if (value != uploading_) {
+            uploading_ = value;
+            emit uploadingChanged(value);
         }
+    }
+
+    void updateAtRoom(const QString &t);
 
-        QTimer typingRefresh_;
-        QTimer typingTimeout_;
-        TimelineModel *room;
-        std::deque<QString> history_;
-        std::size_t history_index_ = 0;
-        int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
-        bool uploading_ = false;
+    QTimer typingRefresh_;
+    QTimer typingTimeout_;
+    TimelineModel *room;
+    std::deque<QString> history_;
+    std::size_t history_index_ = 0;
+    int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
+    bool uploading_      = false;
+    bool containsAtRoom_ = false;
 };
diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp
index e495704514e851bda0c9eba21b2685dc2de67552..4e45f2e2a0a6a2a685aed13d42a26e70e3bc51d1 100644
--- a/src/timeline/Permissions.cpp
+++ b/src/timeline/Permissions.cpp
@@ -12,52 +12,59 @@ Permissions::Permissions(QString roomId, QObject *parent)
   : QObject(parent)
   , roomId_(roomId)
 {
-        invalidate();
+    invalidate();
 }
 
 void
 Permissions::invalidate()
 {
-        pl = cache::client()
-               ->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString())
-               .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
-               .content;
+    pl = cache::client()
+           ->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString())
+           .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
+           .content;
 }
 
 bool
 Permissions::canInvite()
 {
-        return pl.user_level(http::client()->user_id().to_string()) >= pl.invite;
+    return pl.user_level(http::client()->user_id().to_string()) >= pl.invite;
 }
 
 bool
 Permissions::canBan()
 {
-        return pl.user_level(http::client()->user_id().to_string()) >= pl.ban;
+    return pl.user_level(http::client()->user_id().to_string()) >= pl.ban;
 }
 
 bool
 Permissions::canKick()
 {
-        return pl.user_level(http::client()->user_id().to_string()) >= pl.kick;
+    return pl.user_level(http::client()->user_id().to_string()) >= pl.kick;
 }
 
 bool
 Permissions::canRedact()
 {
-        return pl.user_level(http::client()->user_id().to_string()) >= pl.redact;
+    return pl.user_level(http::client()->user_id().to_string()) >= pl.redact;
 }
 bool
 Permissions::canChange(int eventType)
 {
-        return pl.user_level(http::client()->user_id().to_string()) >=
-               pl.state_level(to_string(qml_mtx_events::fromRoomEventType(
-                 static_cast<qml_mtx_events::EventType>(eventType))));
+    return pl.user_level(http::client()->user_id().to_string()) >=
+           pl.state_level(to_string(
+             qml_mtx_events::fromRoomEventType(static_cast<qml_mtx_events::EventType>(eventType))));
 }
 bool
 Permissions::canSend(int eventType)
 {
-        return pl.user_level(http::client()->user_id().to_string()) >=
-               pl.event_level(to_string(qml_mtx_events::fromRoomEventType(
-                 static_cast<qml_mtx_events::EventType>(eventType))));
+    return pl.user_level(http::client()->user_id().to_string()) >=
+           pl.event_level(to_string(
+             qml_mtx_events::fromRoomEventType(static_cast<qml_mtx_events::EventType>(eventType))));
+}
+
+bool
+Permissions::canPingRoom()
+{
+    return pl.user_level(http::client()->user_id().to_string()) >=
+           pl.notification_level(mtx::events::state::notification_keys::room);
 }
diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h
index 7aab1ddb83bafb6ef227188e6130595cab133613..b80a66aa4cef6c84ee5b45fda0e1cce2144704dd 100644
--- a/src/timeline/Permissions.h
+++ b/src/timeline/Permissions.h
@@ -12,22 +12,24 @@ class TimelineModel;
 
 class Permissions : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        Permissions(QString roomId, QObject *parent = nullptr);
+    Permissions(QString roomId, QObject *parent = nullptr);
 
-        Q_INVOKABLE bool canInvite();
-        Q_INVOKABLE bool canBan();
-        Q_INVOKABLE bool canKick();
+    Q_INVOKABLE bool canInvite();
+    Q_INVOKABLE bool canBan();
+    Q_INVOKABLE bool canKick();
 
-        Q_INVOKABLE bool canRedact();
-        Q_INVOKABLE bool canChange(int eventType);
-        Q_INVOKABLE bool canSend(int eventType);
+    Q_INVOKABLE bool canRedact();
+    Q_INVOKABLE bool canChange(int eventType);
+    Q_INVOKABLE bool canSend(int eventType);
 
-        void invalidate();
+    Q_INVOKABLE bool canPingRoom();
+
+    void invalidate();
 
 private:
-        QString roomId_;
-        mtx::events::state::PowerLevels pl;
+    QString roomId_;
+    mtx::events::state::PowerLevels pl;
 };
diff --git a/src/timeline/Reaction.h b/src/timeline/Reaction.h
index 47dac6172b28f627ea038d7c24b5e724244c1c4d..fcdd61a40b984eedb721c26caf934065cacf9e28 100644
--- a/src/timeline/Reaction.h
+++ b/src/timeline/Reaction.h
@@ -9,20 +9,20 @@
 
 struct Reaction
 {
-        Q_GADGET
-        Q_PROPERTY(QString key READ key)
-        Q_PROPERTY(QString users READ users)
-        Q_PROPERTY(QString selfReactedEvent READ selfReactedEvent)
-        Q_PROPERTY(int count READ count)
+    Q_GADGET
+    Q_PROPERTY(QString key READ key)
+    Q_PROPERTY(QString users READ users)
+    Q_PROPERTY(QString selfReactedEvent READ selfReactedEvent)
+    Q_PROPERTY(int count READ count)
 
 public:
-        QString key() const { return key_; }
-        QString users() const { return users_; }
-        QString selfReactedEvent() const { return selfReactedEvent_; }
-        int count() const { return count_; }
+    QString key() const { return key_.toHtmlEscaped(); }
+    QString users() const { return users_.toHtmlEscaped(); }
+    QString selfReactedEvent() const { return selfReactedEvent_; }
+    int count() const { return count_; }
 
-        QString key_;
-        QString users_;
-        QString selfReactedEvent_;
-        int count_;
+    QString key_;
+    QString users_;
+    QString selfReactedEvent_;
+    int count_;
 };
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index f4c927acefc4f26bbc638781f7df5ee82bd075f8..179c63af642f81e6da9560536de3b4672567fc50 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -17,912 +17,967 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent)
   : QAbstractListModel(parent)
   , manager(parent)
 {
-        [[maybe_unused]] static auto id = qRegisterMetaType<RoomPreview>();
-
-        connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
-                auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
-                QHash<QString, QSharedPointer<TimelineModel>>::iterator i;
-                for (i = models.begin(); i != models.end(); ++i) {
-                        auto ptr = i.value();
-
-                        if (!ptr.isNull()) {
-                                ptr->setDecryptDescription(decrypt);
-                                ptr->updateLastMessage();
-                        }
-                }
-        });
-
-        connect(this,
-                &RoomlistModel::totalUnreadMessageCountUpdated,
-                ChatPage::instance(),
-                &ChatPage::unreadMessages);
-
-        connect(
-          this,
-          &RoomlistModel::fetchedPreview,
-          this,
-          [this](QString roomid, RoomInfo info) {
-                  if (this->previewedRooms.contains(roomid)) {
-                          this->previewedRooms.insert(roomid, std::move(info));
-                          auto idx = this->roomidToIndex(roomid);
-                          emit dataChanged(index(idx),
-                                           index(idx),
-                                           {
-                                             Roles::RoomName,
-                                             Roles::AvatarUrl,
-                                             Roles::IsSpace,
-                                             Roles::IsPreviewFetched,
-                                             Qt::DisplayRole,
-                                           });
-                  }
-          },
-          Qt::QueuedConnection);
+    [[maybe_unused]] static auto id = qRegisterMetaType<RoomPreview>();
+
+    connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
+        auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
+        QHash<QString, QSharedPointer<TimelineModel>>::iterator i;
+        for (i = models.begin(); i != models.end(); ++i) {
+            auto ptr = i.value();
+
+            if (!ptr.isNull()) {
+                ptr->setDecryptDescription(decrypt);
+                ptr->updateLastMessage();
+            }
+        }
+    });
+
+    connect(this,
+            &RoomlistModel::totalUnreadMessageCountUpdated,
+            ChatPage::instance(),
+            &ChatPage::unreadMessages);
+
+    connect(
+      this,
+      &RoomlistModel::fetchedPreview,
+      this,
+      [this](QString roomid, RoomInfo info) {
+          if (this->previewedRooms.contains(roomid)) {
+              this->previewedRooms.insert(roomid, std::move(info));
+              auto idx = this->roomidToIndex(roomid);
+              emit dataChanged(index(idx),
+                               index(idx),
+                               {
+                                 Roles::RoomName,
+                                 Roles::AvatarUrl,
+                                 Roles::IsSpace,
+                                 Roles::IsPreviewFetched,
+                                 Qt::DisplayRole,
+                               });
+          }
+      },
+      Qt::QueuedConnection);
 }
 
 QHash<int, QByteArray>
 RoomlistModel::roleNames() const
 {
-        return {
-          {AvatarUrl, "avatarUrl"},
-          {RoomName, "roomName"},
-          {RoomId, "roomId"},
-          {LastMessage, "lastMessage"},
-          {Time, "time"},
-          {Timestamp, "timestamp"},
-          {HasUnreadMessages, "hasUnreadMessages"},
-          {HasLoudNotification, "hasLoudNotification"},
-          {NotificationCount, "notificationCount"},
-          {IsInvite, "isInvite"},
-          {IsSpace, "isSpace"},
-          {Tags, "tags"},
-          {ParentSpaces, "parentSpaces"},
-        };
+    return {
+      {AvatarUrl, "avatarUrl"},
+      {RoomName, "roomName"},
+      {RoomId, "roomId"},
+      {LastMessage, "lastMessage"},
+      {Time, "time"},
+      {Timestamp, "timestamp"},
+      {HasUnreadMessages, "hasUnreadMessages"},
+      {HasLoudNotification, "hasLoudNotification"},
+      {NotificationCount, "notificationCount"},
+      {IsInvite, "isInvite"},
+      {IsSpace, "isSpace"},
+      {Tags, "tags"},
+      {ParentSpaces, "parentSpaces"},
+      {IsDirect, "isDirect"},
+      {DirectChatOtherUserId, "directChatOtherUserId"},
+    };
 }
 
 QVariant
 RoomlistModel::data(const QModelIndex &index, int role) const
 {
-        if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) {
-                auto roomid = roomids.at(index.row());
-
-                if (role == Roles::ParentSpaces) {
-                        auto parents = cache::client()->getParentRoomIds(roomid.toStdString());
-                        QStringList list;
-                        for (const auto &t : parents)
-                                list.push_back(QString::fromStdString(t));
-                        return list;
-                } else if (role == Roles::RoomId) {
-                        return roomid;
-                }
+    if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) {
+        auto roomid = roomids.at(index.row());
+
+        if (role == Roles::ParentSpaces) {
+            auto parents = cache::client()->getParentRoomIds(roomid.toStdString());
+            QStringList list;
+            for (const auto &t : parents)
+                list.push_back(QString::fromStdString(t));
+            return list;
+        } else if (role == Roles::RoomId) {
+            return roomid;
+        }
 
-                if (models.contains(roomid)) {
-                        auto room = models.value(roomid);
-                        switch (role) {
-                        case Roles::AvatarUrl:
-                                return room->roomAvatarUrl();
-                        case Roles::RoomName:
-                                return room->plainRoomName();
-                        case Roles::LastMessage:
-                                return room->lastMessage().body;
-                        case Roles::Time:
-                                return room->lastMessage().descriptiveTime;
-                        case Roles::Timestamp:
-                                return QVariant(
-                                  static_cast<quint64>(room->lastMessage().timestamp));
-                        case Roles::HasUnreadMessages:
-                                return this->roomReadStatus.count(roomid) &&
-                                       this->roomReadStatus.at(roomid);
-                        case Roles::HasLoudNotification:
-                                return room->hasMentions();
-                        case Roles::NotificationCount:
-                                return room->notificationCount();
-                        case Roles::IsInvite:
-                                return false;
-                        case Roles::IsSpace:
-                                return room->isSpace();
-                        case Roles::IsPreview:
-                                return false;
-                        case Roles::Tags: {
-                                auto info = cache::singleRoomInfo(roomid.toStdString());
-                                QStringList list;
-                                for (const auto &t : info.tags)
-                                        list.push_back(QString::fromStdString(t));
-                                return list;
-                        }
-                        default:
-                                return {};
-                        }
-                } else if (invites.contains(roomid)) {
-                        auto room = invites.value(roomid);
-                        switch (role) {
-                        case Roles::AvatarUrl:
-                                return QString::fromStdString(room.avatar_url);
-                        case Roles::RoomName:
-                                return QString::fromStdString(room.name);
-                        case Roles::LastMessage:
-                                return tr("Pending invite.");
-                        case Roles::Time:
-                                return QString();
-                        case Roles::Timestamp:
-                                return QVariant(static_cast<quint64>(0));
-                        case Roles::HasUnreadMessages:
-                        case Roles::HasLoudNotification:
-                                return false;
-                        case Roles::NotificationCount:
-                                return 0;
-                        case Roles::IsInvite:
-                                return true;
-                        case Roles::IsSpace:
-                                return false;
-                        case Roles::IsPreview:
-                                return false;
-                        case Roles::Tags:
-                                return QStringList();
-                        default:
-                                return {};
-                        }
-                } else if (previewedRooms.contains(roomid) &&
-                           previewedRooms.value(roomid).has_value()) {
-                        auto room = previewedRooms.value(roomid).value();
-                        switch (role) {
-                        case Roles::AvatarUrl:
-                                return QString::fromStdString(room.avatar_url);
-                        case Roles::RoomName:
-                                return QString::fromStdString(room.name);
-                        case Roles::LastMessage:
-                                return tr("Previewing this room");
-                        case Roles::Time:
-                                return QString();
-                        case Roles::Timestamp:
-                                return QVariant(static_cast<quint64>(0));
-                        case Roles::HasUnreadMessages:
-                        case Roles::HasLoudNotification:
-                                return false;
-                        case Roles::NotificationCount:
-                                return 0;
-                        case Roles::IsInvite:
-                                return false;
-                        case Roles::IsSpace:
-                                return room.is_space;
-                        case Roles::IsPreview:
-                                return true;
-                        case Roles::IsPreviewFetched:
-                                return true;
-                        case Roles::Tags:
-                                return QStringList();
-                        default:
-                                return {};
-                        }
-                } else {
-                        if (role == Roles::IsPreview)
-                                return true;
-                        else if (role == Roles::IsPreviewFetched)
-                                return false;
-
-                        fetchPreview(roomid);
-                        switch (role) {
-                        case Roles::AvatarUrl:
-                                return QString();
-                        case Roles::RoomName:
-                                return tr("No preview available");
-                        case Roles::LastMessage:
-                                return QString();
-                        case Roles::Time:
-                                return QString();
-                        case Roles::Timestamp:
-                                return QVariant(static_cast<quint64>(0));
-                        case Roles::HasUnreadMessages:
-                        case Roles::HasLoudNotification:
-                                return false;
-                        case Roles::NotificationCount:
-                                return 0;
-                        case Roles::IsInvite:
-                                return false;
-                        case Roles::IsSpace:
-                                return false;
-                        case Roles::Tags:
-                                return QStringList();
-                        default:
-                                return {};
-                        }
-                }
+        if (models.contains(roomid)) {
+            auto room = models.value(roomid);
+            switch (role) {
+            case Roles::AvatarUrl:
+                return room->roomAvatarUrl();
+            case Roles::RoomName:
+                return room->plainRoomName();
+            case Roles::LastMessage:
+                return room->lastMessage().body;
+            case Roles::Time:
+                return room->lastMessage().descriptiveTime;
+            case Roles::Timestamp:
+                return QVariant(static_cast<quint64>(room->lastMessage().timestamp));
+            case Roles::HasUnreadMessages:
+                return this->roomReadStatus.count(roomid) && this->roomReadStatus.at(roomid);
+            case Roles::HasLoudNotification:
+                return room->hasMentions();
+            case Roles::NotificationCount:
+                return room->notificationCount();
+            case Roles::IsInvite:
+                return false;
+            case Roles::IsSpace:
+                return room->isSpace();
+            case Roles::IsPreview:
+                return false;
+            case Roles::Tags: {
+                auto info = cache::singleRoomInfo(roomid.toStdString());
+                QStringList list;
+                for (const auto &t : info.tags)
+                    list.push_back(QString::fromStdString(t));
+                return list;
+            }
+            case Roles::IsDirect:
+                return room->isDirect();
+            case Roles::DirectChatOtherUserId:
+                return room->directChatOtherUserId();
+            default:
+                return {};
+            }
+        } else if (invites.contains(roomid)) {
+            auto room = invites.value(roomid);
+            switch (role) {
+            case Roles::AvatarUrl:
+                return QString::fromStdString(room.avatar_url);
+            case Roles::RoomName:
+                return QString::fromStdString(room.name);
+            case Roles::LastMessage:
+                return tr("Pending invite.");
+            case Roles::Time:
+                return QString();
+            case Roles::Timestamp:
+                return QVariant(static_cast<quint64>(0));
+            case Roles::HasUnreadMessages:
+            case Roles::HasLoudNotification:
+                return false;
+            case Roles::NotificationCount:
+                return 0;
+            case Roles::IsInvite:
+                return true;
+            case Roles::IsSpace:
+                return false;
+            case Roles::IsPreview:
+                return false;
+            case Roles::Tags:
+                return QStringList();
+            case Roles::IsDirect:
+                // The list of users from the room doesn't contain the invited
+                // users, so we won't factor the invite into the count
+                return room.member_count == 1;
+            case Roles::DirectChatOtherUserId:
+                return cache::getMembersFromInvite(roomid.toStdString(), 0, 1).front().user_id;
+            default:
+                return {};
+            }
+        } else if (previewedRooms.contains(roomid) && previewedRooms.value(roomid).has_value()) {
+            auto room = previewedRooms.value(roomid).value();
+            switch (role) {
+            case Roles::AvatarUrl:
+                return QString::fromStdString(room.avatar_url);
+            case Roles::RoomName:
+                return QString::fromStdString(room.name);
+            case Roles::LastMessage:
+                return tr("Previewing this room");
+            case Roles::Time:
+                return QString();
+            case Roles::Timestamp:
+                return QVariant(static_cast<quint64>(0));
+            case Roles::HasUnreadMessages:
+            case Roles::HasLoudNotification:
+                return false;
+            case Roles::NotificationCount:
+                return 0;
+            case Roles::IsInvite:
+                return false;
+            case Roles::IsSpace:
+                return room.is_space;
+            case Roles::IsPreview:
+                return true;
+            case Roles::IsPreviewFetched:
+                return true;
+            case Roles::Tags:
+                return QStringList();
+            case Roles::IsDirect:
+                return false;
+            case Roles::DirectChatOtherUserId:
+                return QString{}; // should never be reached
+            default:
+                return {};
+            }
         } else {
+            if (role == Roles::IsPreview)
+                return true;
+            else if (role == Roles::IsPreviewFetched)
+                return false;
+
+            fetchPreview(roomid);
+            switch (role) {
+            case Roles::AvatarUrl:
+                return QString();
+            case Roles::RoomName:
+                return tr("No preview available");
+            case Roles::LastMessage:
+                return QString();
+            case Roles::Time:
+                return QString();
+            case Roles::Timestamp:
+                return QVariant(static_cast<quint64>(0));
+            case Roles::HasUnreadMessages:
+            case Roles::HasLoudNotification:
+                return false;
+            case Roles::NotificationCount:
+                return 0;
+            case Roles::IsInvite:
+                return false;
+            case Roles::IsSpace:
+                return false;
+            case Roles::Tags:
+                return QStringList();
+            default:
                 return {};
+            }
         }
+    } else {
+        return {};
+    }
 }
 
 void
 RoomlistModel::updateReadStatus(const std::map<QString, bool> roomReadStatus_)
 {
-        std::vector<int> roomsToUpdate;
-        roomsToUpdate.resize(roomReadStatus_.size());
-        for (const auto &[roomid, roomUnread] : roomReadStatus_) {
-                if (roomUnread != roomReadStatus[roomid]) {
-                        roomsToUpdate.push_back(this->roomidToIndex(roomid));
-                }
-
-                this->roomReadStatus[roomid] = roomUnread;
+    std::vector<int> roomsToUpdate;
+    roomsToUpdate.resize(roomReadStatus_.size());
+    for (const auto &[roomid, roomUnread] : roomReadStatus_) {
+        if (roomUnread != roomReadStatus[roomid]) {
+            roomsToUpdate.push_back(this->roomidToIndex(roomid));
         }
 
-        for (auto idx : roomsToUpdate) {
-                emit dataChanged(index(idx),
-                                 index(idx),
-                                 {
-                                   Roles::HasUnreadMessages,
-                                 });
-        }
+        this->roomReadStatus[roomid] = roomUnread;
+    }
+
+    for (auto idx : roomsToUpdate) {
+        emit dataChanged(index(idx),
+                         index(idx),
+                         {
+                           Roles::HasUnreadMessages,
+                         });
+    }
 }
 void
 RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
 {
-        if (!models.contains(room_id)) {
-                // ensure we get read status updates and are only connected once
-                connect(cache::client(),
-                        &Cache::roomReadStatus,
-                        this,
-                        &RoomlistModel::updateReadStatus,
-                        Qt::UniqueConnection);
-
-                QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id));
-                newRoom->setDecryptDescription(
-                  ChatPage::instance()->userSettings()->decryptSidebar());
-
-                connect(newRoom.data(),
-                        &TimelineModel::newEncryptedImage,
-                        manager->imageProvider(),
-                        &MxcImageProvider::addEncryptionInfo);
-                connect(newRoom.data(),
-                        &TimelineModel::forwardToRoom,
-                        manager,
-                        &TimelineViewManager::forwardMessageToRoom);
-                connect(
-                  newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() {
-                          auto idx = this->roomidToIndex(room_id);
-                          emit dataChanged(index(idx),
-                                           index(idx),
-                                           {
-                                             Roles::HasLoudNotification,
-                                             Roles::LastMessage,
-                                             Roles::Timestamp,
-                                             Roles::NotificationCount,
-                                             Qt::DisplayRole,
-                                           });
-                  });
-                connect(
-                  newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() {
-                          auto idx = this->roomidToIndex(room_id);
-                          emit dataChanged(index(idx),
-                                           index(idx),
-                                           {
-                                             Roles::AvatarUrl,
-                                           });
-                  });
-                connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() {
-                        auto idx = this->roomidToIndex(room_id);
-                        emit dataChanged(index(idx),
-                                         index(idx),
-                                         {
-                                           Roles::RoomName,
-                                         });
-                });
-                connect(
-                  newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() {
-                          auto idx = this->roomidToIndex(room_id);
-                          emit dataChanged(index(idx),
-                                           index(idx),
-                                           {
-                                             Roles::HasLoudNotification,
-                                             Roles::NotificationCount,
-                                             Qt::DisplayRole,
-                                           });
-
-                          int total_unread_msgs = 0;
-
-                          for (const auto &room : models) {
-                                  if (!room.isNull())
-                                          total_unread_msgs += room->notificationCount();
-                          }
-
-                          emit totalUnreadMessageCountUpdated(total_unread_msgs);
-                  });
+    if (!models.contains(room_id)) {
+        // ensure we get read status updates and are only connected once
+        connect(cache::client(),
+                &Cache::roomReadStatus,
+                this,
+                &RoomlistModel::updateReadStatus,
+                Qt::UniqueConnection);
+
+        QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id));
+        newRoom->setDecryptDescription(ChatPage::instance()->userSettings()->decryptSidebar());
+
+        connect(newRoom.data(),
+                &TimelineModel::newEncryptedImage,
+                manager->imageProvider(),
+                &MxcImageProvider::addEncryptionInfo);
+        connect(newRoom.data(),
+                &TimelineModel::forwardToRoom,
+                manager,
+                &TimelineViewManager::forwardMessageToRoom);
+        connect(newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() {
+            auto idx = this->roomidToIndex(room_id);
+            emit dataChanged(index(idx),
+                             index(idx),
+                             {
+                               Roles::HasLoudNotification,
+                               Roles::LastMessage,
+                               Roles::Timestamp,
+                               Roles::NotificationCount,
+                               Qt::DisplayRole,
+                             });
+        });
+        connect(newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() {
+            auto idx = this->roomidToIndex(room_id);
+            emit dataChanged(index(idx),
+                             index(idx),
+                             {
+                               Roles::AvatarUrl,
+                             });
+        });
+        connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() {
+            auto idx = this->roomidToIndex(room_id);
+            emit dataChanged(index(idx),
+                             index(idx),
+                             {
+                               Roles::RoomName,
+                             });
+        });
+        connect(newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() {
+            auto idx = this->roomidToIndex(room_id);
+            emit dataChanged(index(idx),
+                             index(idx),
+                             {
+                               Roles::HasLoudNotification,
+                               Roles::NotificationCount,
+                               Qt::DisplayRole,
+                             });
+
+            int total_unread_msgs = 0;
+
+            for (const auto &room : models) {
+                if (!room.isNull())
+                    total_unread_msgs += room->notificationCount();
+            }
+
+            emit totalUnreadMessageCountUpdated(total_unread_msgs);
+        });
 
-                newRoom->updateLastMessage();
-
-                std::vector<QString> previewsToAdd;
-                if (newRoom->isSpace()) {
-                        auto childs = cache::client()->getChildRoomIds(room_id.toStdString());
-                        for (const auto &c : childs) {
-                                auto id = QString::fromStdString(c);
-                                if (!(models.contains(id) || invites.contains(id) ||
-                                      previewedRooms.contains(id))) {
-                                        previewsToAdd.push_back(std::move(id));
-                                }
-                        }
-                }
+        newRoom->updateLastMessage();
 
-                bool wasInvite  = invites.contains(room_id);
-                bool wasPreview = previewedRooms.contains(room_id);
-                if (!suppressInsertNotification &&
-                    ((!wasInvite && !wasPreview) || !previewedRooms.empty()))
-                        // if the old room was already in the list, don't add it. Also add all
-                        // previews at the same time.
-                        beginInsertRows(QModelIndex(),
-                                        (int)roomids.size(),
-                                        (int)(roomids.size() + previewsToAdd.size() -
-                                              ((wasInvite || wasPreview) ? 1 : 0)));
-
-                models.insert(room_id, std::move(newRoom));
-                if (wasInvite) {
-                        auto idx = roomidToIndex(room_id);
-                        invites.remove(room_id);
-                        emit dataChanged(index(idx), index(idx));
-                } else if (wasPreview) {
-                        auto idx = roomidToIndex(room_id);
-                        previewedRooms.remove(room_id);
-                        emit dataChanged(index(idx), index(idx));
-                } else {
-                        roomids.push_back(room_id);
+        std::vector<QString> previewsToAdd;
+        if (newRoom->isSpace()) {
+            auto childs = cache::client()->getChildRoomIds(room_id.toStdString());
+            for (const auto &c : childs) {
+                auto id = QString::fromStdString(c);
+                if (!(models.contains(id) || invites.contains(id) || previewedRooms.contains(id))) {
+                    previewsToAdd.push_back(std::move(id));
                 }
+            }
+        }
 
-                if ((wasInvite || wasPreview) && currentRoomPreview_ &&
-                    currentRoomPreview_->roomid() == room_id) {
-                        currentRoom_ = models.value(room_id);
-                        currentRoomPreview_.reset();
-                        emit currentRoomChanged();
-                }
+        bool wasInvite  = invites.contains(room_id);
+        bool wasPreview = previewedRooms.contains(room_id);
+        if (!suppressInsertNotification && ((!wasInvite && !wasPreview) || !previewedRooms.empty()))
+            // if the old room was already in the list, don't add it. Also add all
+            // previews at the same time.
+            beginInsertRows(
+              QModelIndex(),
+              (int)roomids.size(),
+              (int)(roomids.size() + previewsToAdd.size() - ((wasInvite || wasPreview) ? 1 : 0)));
+
+        models.insert(room_id, std::move(newRoom));
+        if (wasInvite) {
+            auto idx = roomidToIndex(room_id);
+            invites.remove(room_id);
+            emit dataChanged(index(idx), index(idx));
+        } else if (wasPreview) {
+            auto idx = roomidToIndex(room_id);
+            previewedRooms.remove(room_id);
+            emit dataChanged(index(idx), index(idx));
+        } else {
+            roomids.push_back(room_id);
+        }
 
-                for (auto p : previewsToAdd) {
-                        previewedRooms.insert(p, std::nullopt);
-                        roomids.push_back(std::move(p));
-                }
+        if ((wasInvite || wasPreview) && currentRoomPreview_ &&
+            currentRoomPreview_->roomid() == room_id) {
+            currentRoom_ = models.value(room_id);
+            currentRoomPreview_.reset();
+            emit currentRoomChanged();
+        }
 
-                if (!suppressInsertNotification &&
-                    ((!wasInvite && !wasPreview) || !previewedRooms.empty()))
-                        endInsertRows();
+        for (auto p : previewsToAdd) {
+            previewedRooms.insert(p, std::nullopt);
+            roomids.push_back(std::move(p));
         }
+
+        if (!suppressInsertNotification && ((!wasInvite && !wasPreview) || !previewedRooms.empty()))
+            endInsertRows();
+
+        emit ChatPage::instance()->newRoom(room_id);
+    }
 }
 
 void
 RoomlistModel::fetchPreview(QString roomid_) const
 {
-        std::string roomid = roomid_.toStdString();
-        http::client()->get_state_event<mtx::events::state::Create>(
-          roomid,
-          "",
-          [this, roomid](const mtx::events::state::Create &c, mtx::http::RequestErr err) {
-                  bool is_space = false;
-                  if (!err) {
-                          is_space = c.type == mtx::events::state::room_type::space;
-                  }
-
-                  http::client()->get_state_event<mtx::events::state::Avatar>(
-                    roomid,
-                    "",
-                    [this, roomid, is_space](const mtx::events::state::Avatar &a,
-                                             mtx::http::RequestErr) {
-                            auto avatar_url = a.url;
-
-                            http::client()->get_state_event<mtx::events::state::Topic>(
-                              roomid,
-                              "",
-                              [this, roomid, avatar_url, is_space](
-                                const mtx::events::state::Topic &t, mtx::http::RequestErr) {
-                                      auto topic = t.topic;
-                                      http::client()->get_state_event<mtx::events::state::Name>(
-                                        roomid,
-                                        "",
-                                        [this, roomid, topic, avatar_url, is_space](
-                                          const mtx::events::state::Name &n,
-                                          mtx::http::RequestErr err) {
-                                                if (err) {
-                                                        nhlog::net()->warn(
-                                                          "Failed to fetch name event to "
-                                                          "create preview for {}",
-                                                          roomid);
-                                                }
-
-                                                // don't even add a preview, if we got not a single
-                                                // response
-                                                if (n.name.empty() && avatar_url.empty() &&
-                                                    topic.empty())
-                                                        return;
-
-                                                RoomInfo info{};
-                                                info.name       = n.name;
-                                                info.is_space   = is_space;
-                                                info.avatar_url = avatar_url;
-                                                info.topic      = topic;
-
-                                                const_cast<RoomlistModel *>(this)->fetchedPreview(
-                                                  QString::fromStdString(roomid), info);
-                                        });
-                              });
-                    });
-          });
+    std::string roomid = roomid_.toStdString();
+    http::client()->get_state_event<mtx::events::state::Create>(
+      roomid, "", [this, roomid](const mtx::events::state::Create &c, mtx::http::RequestErr err) {
+          bool is_space = false;
+          if (!err) {
+              is_space = c.type == mtx::events::state::room_type::space;
+          }
+
+          http::client()->get_state_event<mtx::events::state::Avatar>(
+            roomid,
+            "",
+            [this, roomid, is_space](const mtx::events::state::Avatar &a, mtx::http::RequestErr) {
+                auto avatar_url = a.url;
+
+                http::client()->get_state_event<mtx::events::state::Topic>(
+                  roomid,
+                  "",
+                  [this, roomid, avatar_url, is_space](const mtx::events::state::Topic &t,
+                                                       mtx::http::RequestErr) {
+                      auto topic = t.topic;
+                      http::client()->get_state_event<mtx::events::state::Name>(
+                        roomid,
+                        "",
+                        [this, roomid, topic, avatar_url, is_space](
+                          const mtx::events::state::Name &n, mtx::http::RequestErr err) {
+                            if (err) {
+                                nhlog::net()->warn("Failed to fetch name event to "
+                                                   "create preview for {}",
+                                                   roomid);
+                            }
+
+                            // don't even add a preview, if we got not a single
+                            // response
+                            if (n.name.empty() && avatar_url.empty() && topic.empty())
+                                return;
+
+                            RoomInfo info{};
+                            info.name       = n.name;
+                            info.is_space   = is_space;
+                            info.avatar_url = avatar_url;
+                            info.topic      = topic;
+
+                            const_cast<RoomlistModel *>(this)->fetchedPreview(
+                              QString::fromStdString(roomid), info);
+                        });
+                  });
+            });
+      });
 }
 
 void
 RoomlistModel::sync(const mtx::responses::Rooms &rooms)
 {
-        for (const auto &[room_id, room] : rooms.join) {
-                auto qroomid = QString::fromStdString(room_id);
-
-                // addRoom will only add the room, if it doesn't exist
-                addRoom(qroomid);
-                const auto &room_model = models.value(qroomid);
-                room_model->sync(room);
-                // room_model->addEvents(room.timeline);
-                connect(room_model.data(),
-                        &TimelineModel::newCallEvent,
-                        manager->callManager(),
-                        &CallManager::syncEvent,
-                        Qt::UniqueConnection);
-
-                if (ChatPage::instance()->userSettings()->typingNotifications()) {
-                        for (const auto &ev : room.ephemeral.events) {
-                                if (auto t = std::get_if<
-                                      mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
-                                      &ev)) {
-                                        std::vector<QString> typing;
-                                        typing.reserve(t->content.user_ids.size());
-                                        for (const auto &user : t->content.user_ids) {
-                                                if (user != http::client()->user_id().to_string())
-                                                        typing.push_back(
-                                                          QString::fromStdString(user));
-                                        }
-                                        room_model->updateTypingUsers(typing);
-                                }
-                        }
+    for (const auto &[room_id, room] : rooms.join) {
+        auto qroomid = QString::fromStdString(room_id);
+
+        // addRoom will only add the room, if it doesn't exist
+        addRoom(qroomid);
+        const auto &room_model = models.value(qroomid);
+        room_model->sync(room);
+        // room_model->addEvents(room.timeline);
+        connect(room_model.data(),
+                &TimelineModel::newCallEvent,
+                manager->callManager(),
+                &CallManager::syncEvent,
+                Qt::UniqueConnection);
+
+        if (ChatPage::instance()->userSettings()->typingNotifications()) {
+            for (const auto &ev : room.ephemeral.events) {
+                if (auto t =
+                      std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
+                        &ev)) {
+                    std::vector<QString> typing;
+                    typing.reserve(t->content.user_ids.size());
+                    for (const auto &user : t->content.user_ids) {
+                        if (user != http::client()->user_id().to_string())
+                            typing.push_back(QString::fromStdString(user));
+                    }
+                    room_model->updateTypingUsers(typing);
                 }
+            }
         }
-
-        for (const auto &[room_id, room] : rooms.leave) {
-                (void)room;
-                auto qroomid = QString::fromStdString(room_id);
-
-                if ((currentRoom_ && currentRoom_->roomId() == qroomid) ||
-                    (currentRoomPreview_ && currentRoomPreview_->roomid() == qroomid))
-                        resetCurrentRoom();
-
-                auto idx = this->roomidToIndex(qroomid);
-                if (idx != -1) {
-                        beginRemoveRows(QModelIndex(), idx, idx);
-                        roomids.erase(roomids.begin() + idx);
-                        if (models.contains(qroomid))
-                                models.remove(qroomid);
-                        else if (invites.contains(qroomid))
-                                invites.remove(qroomid);
-                        endRemoveRows();
-                }
+    }
+
+    for (const auto &[room_id, room] : rooms.leave) {
+        (void)room;
+        auto qroomid = QString::fromStdString(room_id);
+
+        if ((currentRoom_ && currentRoom_->roomId() == qroomid) ||
+            (currentRoomPreview_ && currentRoomPreview_->roomid() == qroomid))
+            resetCurrentRoom();
+
+        auto idx = this->roomidToIndex(qroomid);
+        if (idx != -1) {
+            beginRemoveRows(QModelIndex(), idx, idx);
+            roomids.erase(roomids.begin() + idx);
+            if (models.contains(qroomid))
+                models.remove(qroomid);
+            else if (invites.contains(qroomid))
+                invites.remove(qroomid);
+            endRemoveRows();
         }
+    }
 
-        for (const auto &[room_id, room] : rooms.invite) {
-                (void)room;
-                auto qroomid = QString::fromStdString(room_id);
-
-                auto invite = cache::client()->invite(room_id);
-                if (!invite)
-                        continue;
-
-                if (invites.contains(qroomid)) {
-                        invites[qroomid] = *invite;
-                        auto idx         = roomidToIndex(qroomid);
-                        emit dataChanged(index(idx), index(idx));
-                } else {
-                        beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
-                        invites.insert(qroomid, *invite);
-                        roomids.push_back(std::move(qroomid));
-                        endInsertRows();
-                }
+    for (const auto &[room_id, room] : rooms.invite) {
+        (void)room;
+        auto qroomid = QString::fromStdString(room_id);
+
+        auto invite = cache::client()->invite(room_id);
+        if (!invite)
+            continue;
+
+        if (invites.contains(qroomid)) {
+            invites[qroomid] = *invite;
+            auto idx         = roomidToIndex(qroomid);
+            emit dataChanged(index(idx), index(idx));
+        } else {
+            beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
+            invites.insert(qroomid, *invite);
+            roomids.push_back(std::move(qroomid));
+            endInsertRows();
         }
+    }
 }
 
 void
 RoomlistModel::initializeRooms()
 {
-        beginResetModel();
-        models.clear();
-        roomids.clear();
-        invites.clear();
-        currentRoom_ = nullptr;
+    beginResetModel();
+    models.clear();
+    roomids.clear();
+    invites.clear();
+    currentRoom_ = nullptr;
 
-        invites = cache::client()->invites();
-        for (const auto &id : invites.keys())
-                roomids.push_back(id);
+    invites = cache::client()->invites();
+    for (const auto &id : invites.keys())
+        roomids.push_back(id);
 
-        for (const auto &id : cache::client()->roomIds())
-                addRoom(id, true);
+    for (const auto &id : cache::client()->roomIds())
+        addRoom(id, true);
 
-        nhlog::db()->info("Restored {} rooms from cache", rowCount());
+    nhlog::db()->info("Restored {} rooms from cache", rowCount());
 
-        endResetModel();
+    endResetModel();
 }
 
 void
 RoomlistModel::clear()
 {
-        beginResetModel();
-        models.clear();
-        invites.clear();
-        roomids.clear();
-        currentRoom_ = nullptr;
-        emit currentRoomChanged();
-        endResetModel();
+    beginResetModel();
+    models.clear();
+    invites.clear();
+    roomids.clear();
+    currentRoom_ = nullptr;
+    emit currentRoomChanged();
+    endResetModel();
 }
 
 void
 RoomlistModel::joinPreview(QString roomid, QString parentSpace)
 {
-        if (previewedRooms.contains(roomid)) {
-                auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>(
-                  parentSpace.toStdString(), roomid.toStdString());
-                ChatPage::instance()->joinRoomVia(roomid.toStdString(),
-                                                  (child && child->content.via)
-                                                    ? child->content.via.value()
-                                                    : std::vector<std::string>{},
-                                                  false);
-        }
+    if (previewedRooms.contains(roomid)) {
+        auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>(
+          parentSpace.toStdString(), roomid.toStdString());
+        ChatPage::instance()->joinRoomVia(
+          roomid.toStdString(),
+          (child && child->content.via) ? child->content.via.value() : std::vector<std::string>{},
+          false);
+    }
 }
 void
 RoomlistModel::acceptInvite(QString roomid)
 {
-        if (invites.contains(roomid)) {
-                // Don't remove invite yet, so that we can switch to it
-                ChatPage::instance()->joinRoom(roomid);
-        }
+    if (invites.contains(roomid)) {
+        // Don't remove invite yet, so that we can switch to it
+        ChatPage::instance()->joinRoom(roomid);
+    }
 }
 void
 RoomlistModel::declineInvite(QString roomid)
 {
-        if (invites.contains(roomid)) {
-                auto idx = roomidToIndex(roomid);
-
-                if (idx != -1) {
-                        beginRemoveRows(QModelIndex(), idx, idx);
-                        roomids.erase(roomids.begin() + idx);
-                        invites.remove(roomid);
-                        endRemoveRows();
-                        ChatPage::instance()->leaveRoom(roomid);
-                }
+    if (invites.contains(roomid)) {
+        auto idx = roomidToIndex(roomid);
+
+        if (idx != -1) {
+            beginRemoveRows(QModelIndex(), idx, idx);
+            roomids.erase(roomids.begin() + idx);
+            invites.remove(roomid);
+            endRemoveRows();
+            ChatPage::instance()->leaveRoom(roomid);
         }
+    }
 }
 void
 RoomlistModel::leave(QString roomid)
 {
-        if (models.contains(roomid)) {
-                auto idx = roomidToIndex(roomid);
-
-                if (idx != -1) {
-                        beginRemoveRows(QModelIndex(), idx, idx);
-                        roomids.erase(roomids.begin() + idx);
-                        models.remove(roomid);
-                        endRemoveRows();
-                        ChatPage::instance()->leaveRoom(roomid);
-                }
+    if (models.contains(roomid)) {
+        auto idx = roomidToIndex(roomid);
+
+        if (idx != -1) {
+            beginRemoveRows(QModelIndex(), idx, idx);
+            roomids.erase(roomids.begin() + idx);
+            models.remove(roomid);
+            endRemoveRows();
+            ChatPage::instance()->leaveRoom(roomid);
         }
+    }
 }
 
 void
 RoomlistModel::setCurrentRoom(QString roomid)
 {
-        if ((currentRoom_ && currentRoom_->roomId() == roomid) ||
-            (currentRoomPreview_ && currentRoomPreview_->roomid() == roomid))
-                return;
+    if ((currentRoom_ && currentRoom_->roomId() == roomid) ||
+        (currentRoomPreview_ && currentRoomPreview_->roomid() == roomid))
+        return;
 
-        nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString());
-        if (models.contains(roomid)) {
-                currentRoom_ = models.value(roomid);
-                currentRoomPreview_.reset();
-                emit currentRoomChanged();
-                nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
-        } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) {
-                currentRoom_ = nullptr;
-                std::optional<RoomInfo> i;
-
-                RoomPreview p;
-
-                if (invites.contains(roomid)) {
-                        i           = invites.value(roomid);
-                        p.isInvite_ = true;
-                } else {
-                        i           = previewedRooms.value(roomid);
-                        p.isInvite_ = false;
-                }
+    if (roomid.isEmpty()) {
+        currentRoom_        = nullptr;
+        currentRoomPreview_ = {};
+        emit currentRoomChanged();
+    }
 
-                if (i) {
-                        p.roomid_           = roomid;
-                        p.roomName_         = QString::fromStdString(i->name);
-                        p.roomTopic_        = QString::fromStdString(i->topic);
-                        p.roomAvatarUrl_    = QString::fromStdString(i->avatar_url);
-                        currentRoomPreview_ = std::move(p);
-                }
+    nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString());
+    if (models.contains(roomid)) {
+        currentRoom_ = models.value(roomid);
+        currentRoomPreview_.reset();
+        emit currentRoomChanged();
+        nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
+    } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) {
+        currentRoom_ = nullptr;
+        std::optional<RoomInfo> i;
+
+        RoomPreview p;
 
-                emit currentRoomChanged();
-                nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
+        if (invites.contains(roomid)) {
+            i           = invites.value(roomid);
+            p.isInvite_ = true;
+        } else {
+            i           = previewedRooms.value(roomid);
+            p.isInvite_ = false;
         }
+
+        if (i) {
+            p.roomid_           = roomid;
+            p.roomName_         = QString::fromStdString(i->name);
+            p.roomTopic_        = QString::fromStdString(i->topic);
+            p.roomAvatarUrl_    = QString::fromStdString(i->avatar_url);
+            currentRoomPreview_ = std::move(p);
+            nhlog::ui()->debug("Switched to (preview): {}",
+                               currentRoomPreview_->roomid_.toStdString());
+        } else {
+            p.roomid_           = roomid;
+            currentRoomPreview_ = p;
+            nhlog::ui()->debug("Switched to (empty): {}",
+                               currentRoomPreview_->roomid_.toStdString());
+        }
+
+        emit currentRoomChanged();
+    } else {
+        currentRoom_ = nullptr;
+
+        RoomPreview p;
+        p.roomid_           = roomid;
+        currentRoomPreview_ = std::move(p);
+        emit currentRoomChanged();
+        nhlog::ui()->debug("Switched to (empty): {}", roomid.toStdString());
+    }
 }
 
 namespace {
 enum NotificationImportance : short
 {
-        ImportanceDisabled = -3,
-        NoPreview          = -2,
-        Preview            = -1,
-        AllEventsRead      = 0,
-        NewMessage         = 1,
-        NewMentions        = 2,
-        Invite             = 3,
-        SubSpace           = 4,
-        CurrentSpace       = 5,
+    ImportanceDisabled = -3,
+    NoPreview          = -2,
+    Preview            = -1,
+    AllEventsRead      = 0,
+    NewMessage         = 1,
+    NewMentions        = 2,
+    Invite             = 3,
+    SubSpace           = 4,
+    CurrentSpace       = 5,
 };
 }
 
 short int
 FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const
 {
-        // Returns the degree of importance of the unread messages in the room.
-        // If sorting by importance is disabled in settings, this only ever
-        // returns ImportanceDisabled or Invite
-        if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) {
-                if (filterType == FilterBy::Space &&
-                    filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString())
-                        return CurrentSpace;
-                else
-                        return SubSpace;
-        } else if (sourceModel()->data(idx, RoomlistModel::IsPreview).toBool()) {
-                if (sourceModel()->data(idx, RoomlistModel::IsPreviewFetched).toBool())
-                        return Preview;
-                else
-                        return NoPreview;
-        } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
-                return Invite;
-        } else if (!this->sortByImportance) {
-                return ImportanceDisabled;
-        } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) {
-                return NewMentions;
-        } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) {
-                return NewMessage;
-        } else {
-                return AllEventsRead;
-        }
+    // Returns the degree of importance of the unread messages in the room.
+    // If sorting by importance is disabled in settings, this only ever
+    // returns ImportanceDisabled or Invite
+    if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) {
+        if (filterType == FilterBy::Space &&
+            filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString())
+            return CurrentSpace;
+        else
+            return SubSpace;
+    } else if (sourceModel()->data(idx, RoomlistModel::IsPreview).toBool()) {
+        if (sourceModel()->data(idx, RoomlistModel::IsPreviewFetched).toBool())
+            return Preview;
+        else
+            return NoPreview;
+    } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
+        return Invite;
+    } else if (!this->sortByImportance) {
+        return ImportanceDisabled;
+    } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) {
+        return NewMentions;
+    } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) {
+        return NewMessage;
+    } else {
+        return AllEventsRead;
+    }
 }
 
 bool
 FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
 {
-        QModelIndex const left_idx  = sourceModel()->index(left.row(), 0, QModelIndex());
-        QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex());
-
-        // Sort by "importance" (i.e. invites before mentions before
-        // notifs before new events before old events), then secondly
-        // by recency.
-
-        // Checking importance first
-        const auto a_importance = calculateImportance(left_idx);
-        const auto b_importance = calculateImportance(right_idx);
-        if (a_importance != b_importance) {
-                return a_importance > b_importance;
-        }
-
-        // Now sort by recency
-        // Zero if empty, otherwise the time that the event occured
-        uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong();
-        uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong();
-
-        if (a_recency != b_recency)
-                return a_recency > b_recency;
-        else
-                return left.row() < right.row();
+    QModelIndex const left_idx  = sourceModel()->index(left.row(), 0, QModelIndex());
+    QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex());
+
+    // Sort by "importance" (i.e. invites before mentions before
+    // notifs before new events before old events), then secondly
+    // by recency.
+
+    // Checking importance first
+    const auto a_importance = calculateImportance(left_idx);
+    const auto b_importance = calculateImportance(right_idx);
+    if (a_importance != b_importance) {
+        return a_importance > b_importance;
+    }
+
+    // Now sort by recency
+    // Zero if empty, otherwise the time that the event occured
+    uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong();
+    uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong();
+
+    if (a_recency != b_recency)
+        return a_recency > b_recency;
+    else
+        return left.row() < right.row();
 }
 
 FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent)
   : QSortFilterProxyModel(parent)
   , roomlistmodel(model)
 {
-        this->sortByImportance = UserSettings::instance()->sortByImportance();
-        setSourceModel(model);
-        setDynamicSortFilter(true);
-
-        QObject::connect(UserSettings::instance().get(),
-                         &UserSettings::roomSortingChanged,
-                         this,
-                         [this](bool sortByImportance_) {
-                                 this->sortByImportance = sortByImportance_;
-                                 invalidate();
-                         });
-
-        connect(roomlistmodel,
-                &RoomlistModel::currentRoomChanged,
-                this,
-                &FilteredRoomlistModel::currentRoomChanged);
-
-        sort(0);
+    this->sortByImportance = UserSettings::instance()->sortByImportance();
+    setSourceModel(model);
+    setDynamicSortFilter(true);
+
+    QObject::connect(UserSettings::instance().get(),
+                     &UserSettings::roomSortingChanged,
+                     this,
+                     [this](bool sortByImportance_) {
+                         this->sortByImportance = sortByImportance_;
+                         invalidate();
+                     });
+
+    connect(roomlistmodel,
+            &RoomlistModel::currentRoomChanged,
+            this,
+            &FilteredRoomlistModel::currentRoomChanged);
+
+    sort(0);
 }
 
 void
 FilteredRoomlistModel::updateHiddenTagsAndSpaces()
 {
-        hiddenTags.clear();
-        hiddenSpaces.clear();
-        for (const auto &t : UserSettings::instance()->hiddenTags()) {
-                if (t.startsWith("tag:"))
-                        hiddenTags.push_back(t.mid(4));
-                else if (t.startsWith("space:"))
-                        hiddenSpaces.push_back(t.mid(6));
-        }
-
-        invalidateFilter();
+    hiddenTags.clear();
+    hiddenSpaces.clear();
+    for (const auto &t : UserSettings::instance()->hiddenTags()) {
+        if (t.startsWith("tag:"))
+            hiddenTags.push_back(t.mid(4));
+        else if (t.startsWith("space:"))
+            hiddenSpaces.push_back(t.mid(6));
+    }
+
+    invalidateFilter();
 }
 
 bool
 FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
 {
-        if (filterType == FilterBy::Nothing) {
-                if (sourceModel()
-                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview)
-                      .toBool()) {
-                        return false;
-                }
+    if (filterType == FilterBy::Nothing) {
+        if (sourceModel()
+              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview)
+              .toBool()) {
+            return false;
+        }
 
-                if (sourceModel()
-                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
-                      .toBool()) {
-                        return false;
-                }
+        if (sourceModel()
+              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+              .toBool()) {
+            return false;
+        }
 
-                if (!hiddenTags.empty()) {
-                        auto tags =
-                          sourceModel()
-                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
-                            .toStringList();
+        if (!hiddenTags.empty()) {
+            auto tags = sourceModel()
+                          ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                          .toStringList();
 
-                        for (const auto &t : tags)
-                                if (hiddenTags.contains(t))
-                                        return false;
-                }
+            for (const auto &t : tags)
+                if (hiddenTags.contains(t))
+                    return false;
+        }
 
-                if (!hiddenSpaces.empty()) {
-                        auto parents =
-                          sourceModel()
-                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
-                            .toStringList();
-                        for (const auto &t : parents)
-                                if (hiddenSpaces.contains(t))
-                                        return false;
-                }
+        if (!hiddenSpaces.empty()) {
+            auto parents = sourceModel()
+                             ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+                             .toStringList();
+            for (const auto &t : parents)
+                if (hiddenSpaces.contains(t))
+                    return false;
+        }
 
-                return true;
-        } else if (filterType == FilterBy::Tag) {
-                if (sourceModel()
-                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview)
-                      .toBool()) {
-                        return false;
-                }
+        return true;
+    } else if (filterType == FilterBy::Tag) {
+        if (sourceModel()
+              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview)
+              .toBool()) {
+            return false;
+        }
 
-                if (sourceModel()
-                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
-                      .toBool()) {
-                        return false;
-                }
+        if (sourceModel()
+              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+              .toBool()) {
+            return false;
+        }
 
-                auto tags = sourceModel()
-                              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
-                              .toStringList();
+        auto tags = sourceModel()
+                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                      .toStringList();
 
-                if (!tags.contains(filterStr))
-                        return false;
+        if (!tags.contains(filterStr))
+            return false;
 
-                if (!hiddenTags.empty()) {
-                        for (const auto &t : tags)
-                                if (t != filterStr && hiddenTags.contains(t))
-                                        return false;
-                }
+        if (!hiddenTags.empty()) {
+            for (const auto &t : tags)
+                if (t != filterStr && hiddenTags.contains(t))
+                    return false;
+        }
 
-                if (!hiddenSpaces.empty()) {
-                        auto parents =
-                          sourceModel()
-                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
-                            .toStringList();
-                        for (const auto &t : parents)
-                                if (hiddenSpaces.contains(t))
-                                        return false;
-                }
+        if (!hiddenSpaces.empty()) {
+            auto parents = sourceModel()
+                             ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+                             .toStringList();
+            for (const auto &t : parents)
+                if (hiddenSpaces.contains(t))
+                    return false;
+        }
 
-                return true;
-        } else if (filterType == FilterBy::Space) {
-                if (filterStr == sourceModel()
-                                   ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId)
-                                   .toString())
-                        return true;
-
-                auto parents =
-                  sourceModel()
-                    ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
-                    .toStringList();
-
-                if (!parents.contains(filterStr))
-                        return false;
-
-                if (!hiddenTags.empty()) {
-                        auto tags =
-                          sourceModel()
-                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
-                            .toStringList();
-
-                        for (const auto &t : tags)
-                                if (hiddenTags.contains(t))
-                                        return false;
-                }
+        return true;
+    } else if (filterType == FilterBy::Space) {
+        if (filterStr == sourceModel()
+                           ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId)
+                           .toString())
+            return true;
 
-                if (!hiddenSpaces.empty()) {
-                        for (const auto &t : parents)
-                                if (hiddenSpaces.contains(t))
-                                        return false;
-                }
+        auto parents = sourceModel()
+                         ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+                         .toStringList();
 
-                if (sourceModel()
-                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
-                      .toBool() &&
-                    !parents.contains(filterStr)) {
-                        return false;
-                }
+        if (!parents.contains(filterStr))
+            return false;
 
-                return true;
-        } else {
-                return true;
+        if (!hiddenTags.empty()) {
+            auto tags = sourceModel()
+                          ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                          .toStringList();
+
+            for (const auto &t : tags)
+                if (hiddenTags.contains(t))
+                    return false;
+        }
+
+        if (!hiddenSpaces.empty()) {
+            for (const auto &t : parents)
+                if (t != filterStr && hiddenSpaces.contains(t))
+                    return false;
+        }
+
+        if (sourceModel()
+              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+              .toBool() &&
+            !parents.contains(filterStr)) {
+            return false;
         }
+
+        return true;
+    } else {
+        return true;
+    }
 }
 
 void
 FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
 {
-        if (on) {
-                http::client()->put_tag(
-                  roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::ui()->error("Failed to add tag: {}, {}",
-                                                     tag.toStdString(),
-                                                     err->matrix_error.error);
-                          }
-                  });
-        } else {
-                http::client()->delete_tag(
-                  roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::ui()->error("Failed to delete tag: {}, {}",
-                                                     tag.toStdString(),
-                                                     err->matrix_error.error);
-                          }
-                  });
+    if (on) {
+        http::client()->put_tag(
+          roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::ui()->error(
+                    "Failed to add tag: {}, {}", tag.toStdString(), err->matrix_error.error);
+              }
+          });
+    } else {
+        http::client()->delete_tag(
+          roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::ui()->error(
+                    "Failed to delete tag: {}, {}", tag.toStdString(), err->matrix_error.error);
+              }
+          });
+    }
+}
+
+void
+FilteredRoomlistModel::nextRoomWithActivity()
+{
+    int roomWithMention       = -1;
+    int roomWithNotification  = -1;
+    int roomWithUnreadMessage = -1;
+    auto r                    = currentRoom();
+    int currentRoomIdx        = r ? roomidToIndex(r->roomId()) : -1;
+    // first look for mentions
+    for (int i = 0; i < (int)roomlistmodel->roomids.size(); i++) {
+        if (i == currentRoomIdx)
+            continue;
+        if (this->data(index(i, 0), RoomlistModel::HasLoudNotification).toBool()) {
+            roomWithMention = i;
+            break;
+        }
+        if (roomWithNotification == -1 &&
+            this->data(index(i, 0), RoomlistModel::NotificationCount).toInt() > 0) {
+            roomWithNotification = i;
+            // don't break, we must continue looking for rooms with mentions
+        }
+        if (roomWithNotification == -1 && roomWithUnreadMessage == -1 &&
+            this->data(index(i, 0), RoomlistModel::HasUnreadMessages).toBool()) {
+            roomWithUnreadMessage = i;
+            // don't break, we must continue looking for rooms with mentions
         }
+    }
+    QString targetRoomId = nullptr;
+    if (roomWithMention != -1) {
+        targetRoomId = this->data(index(roomWithMention, 0), RoomlistModel::RoomId).toString();
+        nhlog::ui()->debug("choosing {} for mentions", targetRoomId.toStdString());
+    } else if (roomWithNotification != -1) {
+        targetRoomId = this->data(index(roomWithNotification, 0), RoomlistModel::RoomId).toString();
+        nhlog::ui()->debug("choosing {} for notifications", targetRoomId.toStdString());
+    } else if (roomWithUnreadMessage != -1) {
+        targetRoomId =
+          this->data(index(roomWithUnreadMessage, 0), RoomlistModel::RoomId).toString();
+        nhlog::ui()->debug("choosing {} for unread messages", targetRoomId.toStdString());
+    }
+    if (targetRoomId != nullptr) {
+        setCurrentRoom(targetRoomId);
+    }
 }
 
 void
 FilteredRoomlistModel::nextRoom()
 {
-        auto r = currentRoom();
-
-        if (r) {
-                int idx = roomidToIndex(r->roomId());
-                idx++;
-                if (idx < rowCount()) {
-                        setCurrentRoom(
-                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
-                }
+    auto r = currentRoom();
+
+    if (r) {
+        int idx = roomidToIndex(r->roomId());
+        idx++;
+        if (idx < rowCount()) {
+            setCurrentRoom(data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
         }
+    }
 }
 
 void
 FilteredRoomlistModel::previousRoom()
 {
-        auto r = currentRoom();
-
-        if (r) {
-                int idx = roomidToIndex(r->roomId());
-                idx--;
-                if (idx >= 0) {
-                        setCurrentRoom(
-                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
-                }
+    auto r = currentRoom();
+
+    if (r) {
+        int idx = roomidToIndex(r->roomId());
+        idx--;
+        if (idx >= 0) {
+            setCurrentRoom(data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
         }
+    }
 }
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 6ac6da18694ace471402caf4f3976744580ef6e1..458e0fe7e79eefc073aa6b71a5683a5685c54e39 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -20,192 +20,191 @@ class TimelineViewManager;
 
 class RoomPreview
 {
-        Q_GADGET
-        Q_PROPERTY(QString roomid READ roomid CONSTANT)
-        Q_PROPERTY(QString roomName READ roomName CONSTANT)
-        Q_PROPERTY(QString roomTopic READ roomTopic CONSTANT)
-        Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl CONSTANT)
-        Q_PROPERTY(bool isInvite READ isInvite CONSTANT)
+    Q_GADGET
+    Q_PROPERTY(QString roomid READ roomid CONSTANT)
+    Q_PROPERTY(QString roomName READ roomName CONSTANT)
+    Q_PROPERTY(QString roomTopic READ roomTopic CONSTANT)
+    Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl CONSTANT)
+    Q_PROPERTY(bool isInvite READ isInvite CONSTANT)
 
 public:
-        RoomPreview() {}
+    RoomPreview() {}
 
-        QString roomid() const { return roomid_; }
-        QString roomName() const { return roomName_; }
-        QString roomTopic() const { return roomTopic_; }
-        QString roomAvatarUrl() const { return roomAvatarUrl_; }
-        bool isInvite() const { return isInvite_; }
+    QString roomid() const { return roomid_; }
+    QString roomName() const { return roomName_; }
+    QString roomTopic() const { return roomTopic_; }
+    QString roomAvatarUrl() const { return roomAvatarUrl_; }
+    bool isInvite() const { return isInvite_; }
 
-        QString roomid_, roomName_, roomAvatarUrl_, roomTopic_;
-        bool isInvite_ = false;
+    QString roomid_, roomName_, roomAvatarUrl_, roomTopic_;
+    bool isInvite_ = false;
 };
 
 class RoomlistModel : public QAbstractListModel
 {
-        Q_OBJECT
-        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET
-                     resetCurrentRoom)
-        Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged
-                     RESET resetCurrentRoom)
+    Q_OBJECT
+    Q_PROPERTY(
+      TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom)
+    Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged
+                 RESET resetCurrentRoom)
 public:
-        enum Roles
-        {
-                AvatarUrl = Qt::UserRole,
-                RoomName,
-                RoomId,
-                LastMessage,
-                Time,
-                Timestamp,
-                HasUnreadMessages,
-                HasLoudNotification,
-                NotificationCount,
-                IsInvite,
-                IsSpace,
-                IsPreview,
-                IsPreviewFetched,
-                Tags,
-                ParentSpaces,
-        };
-
-        RoomlistModel(TimelineViewManager *parent = nullptr);
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override
-        {
-                (void)parent;
-                return (int)roomids.size();
-        }
-        QVariant data(const QModelIndex &index, int role) const override;
-        QSharedPointer<TimelineModel> getRoomById(QString id) const
-        {
-                if (models.contains(id))
-                        return models.value(id);
-                else
-                        return {};
-        }
+    enum Roles
+    {
+        AvatarUrl = Qt::UserRole,
+        RoomName,
+        RoomId,
+        LastMessage,
+        Time,
+        Timestamp,
+        HasUnreadMessages,
+        HasLoudNotification,
+        NotificationCount,
+        IsInvite,
+        IsSpace,
+        IsPreview,
+        IsPreviewFetched,
+        Tags,
+        ParentSpaces,
+        IsDirect,
+        DirectChatOtherUserId,
+    };
+
+    RoomlistModel(TimelineViewManager *parent = nullptr);
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        (void)parent;
+        return (int)roomids.size();
+    }
+    QVariant data(const QModelIndex &index, int role) const override;
+    QSharedPointer<TimelineModel> getRoomById(QString id) const
+    {
+        if (models.contains(id))
+            return models.value(id);
+        else
+            return {};
+    }
 
 public slots:
-        void initializeRooms();
-        void sync(const mtx::responses::Rooms &rooms);
-        void clear();
-        int roomidToIndex(QString roomid)
-        {
-                for (int i = 0; i < (int)roomids.size(); i++) {
-                        if (roomids[i] == roomid)
-                                return i;
-                }
-
-                return -1;
-        }
-        void joinPreview(QString roomid, QString parentSpace);
-        void acceptInvite(QString roomid);
-        void declineInvite(QString roomid);
-        void leave(QString roomid);
-        TimelineModel *currentRoom() const { return currentRoom_.get(); }
-        RoomPreview currentRoomPreview() const
-        {
-                return currentRoomPreview_.value_or(RoomPreview{});
-        }
-        void setCurrentRoom(QString roomid);
-        void resetCurrentRoom()
-        {
-                currentRoom_ = nullptr;
-                currentRoomPreview_.reset();
-                emit currentRoomChanged();
+    void initializeRooms();
+    void sync(const mtx::responses::Rooms &rooms);
+    void clear();
+    int roomidToIndex(QString roomid)
+    {
+        for (int i = 0; i < (int)roomids.size(); i++) {
+            if (roomids[i] == roomid)
+                return i;
         }
 
+        return -1;
+    }
+    void joinPreview(QString roomid, QString parentSpace);
+    void acceptInvite(QString roomid);
+    void declineInvite(QString roomid);
+    void leave(QString roomid);
+    TimelineModel *currentRoom() const { return currentRoom_.get(); }
+    RoomPreview currentRoomPreview() const { return currentRoomPreview_.value_or(RoomPreview{}); }
+    void setCurrentRoom(QString roomid);
+    void resetCurrentRoom()
+    {
+        currentRoom_ = nullptr;
+        currentRoomPreview_.reset();
+        emit currentRoomChanged();
+    }
+
 private slots:
-        void updateReadStatus(const std::map<QString, bool> roomReadStatus_);
+    void updateReadStatus(const std::map<QString, bool> roomReadStatus_);
 
 signals:
-        void totalUnreadMessageCountUpdated(int unreadMessages);
-        void currentRoomChanged();
-        void fetchedPreview(QString roomid, RoomInfo info);
+    void totalUnreadMessageCountUpdated(int unreadMessages);
+    void currentRoomChanged();
+    void fetchedPreview(QString roomid, RoomInfo info);
 
 private:
-        void addRoom(const QString &room_id, bool suppressInsertNotification = false);
-        void fetchPreview(QString roomid) const;
+    void addRoom(const QString &room_id, bool suppressInsertNotification = false);
+    void fetchPreview(QString roomid) const;
 
-        TimelineViewManager *manager = nullptr;
-        std::vector<QString> roomids;
-        QHash<QString, RoomInfo> invites;
-        QHash<QString, QSharedPointer<TimelineModel>> models;
-        std::map<QString, bool> roomReadStatus;
-        QHash<QString, std::optional<RoomInfo>> previewedRooms;
+    TimelineViewManager *manager = nullptr;
+    std::vector<QString> roomids;
+    QHash<QString, RoomInfo> invites;
+    QHash<QString, QSharedPointer<TimelineModel>> models;
+    std::map<QString, bool> roomReadStatus;
+    QHash<QString, std::optional<RoomInfo>> previewedRooms;
 
-        QSharedPointer<TimelineModel> currentRoom_;
-        std::optional<RoomPreview> currentRoomPreview_;
+    QSharedPointer<TimelineModel> currentRoom_;
+    std::optional<RoomPreview> currentRoomPreview_;
 
-        friend class FilteredRoomlistModel;
+    friend class FilteredRoomlistModel;
 };
 
 class FilteredRoomlistModel : public QSortFilterProxyModel
 {
-        Q_OBJECT
-        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET
-                     resetCurrentRoom)
-        Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged
-                     RESET resetCurrentRoom)
+    Q_OBJECT
+    Q_PROPERTY(
+      TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom)
+    Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged
+                 RESET resetCurrentRoom)
 public:
-        FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
-        bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
-        bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override;
+    FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
+    bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+    bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override;
 
 public slots:
-        int roomidToIndex(QString roomid)
-        {
-                return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid)))
-                  .row();
-        }
-        void joinPreview(QString roomid)
-        {
-                roomlistmodel->joinPreview(roomid, filterType == FilterBy::Space ? filterStr : "");
-        }
-        void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
-        void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
-        void leave(QString roomid) { roomlistmodel->leave(roomid); }
-        void toggleTag(QString roomid, QString tag, bool on);
-
-        TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
-        RoomPreview currentRoomPreview() const { return roomlistmodel->currentRoomPreview(); }
-        void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
-        void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); }
-
-        void nextRoom();
-        void previousRoom();
-
-        void updateFilterTag(QString tagId)
-        {
-                if (tagId.startsWith("tag:")) {
-                        filterType = FilterBy::Tag;
-                        filterStr  = tagId.mid(4);
-                } else if (tagId.startsWith("space:")) {
-                        filterType = FilterBy::Space;
-                        filterStr  = tagId.mid(6);
-                } else {
-                        filterType = FilterBy::Nothing;
-                        filterStr.clear();
-                }
-
-                invalidateFilter();
+    int roomidToIndex(QString roomid)
+    {
+        return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))).row();
+    }
+    void joinPreview(QString roomid)
+    {
+        roomlistmodel->joinPreview(roomid, filterType == FilterBy::Space ? filterStr : "");
+    }
+    void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
+    void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
+    void leave(QString roomid) { roomlistmodel->leave(roomid); }
+    void toggleTag(QString roomid, QString tag, bool on);
+
+    TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
+    RoomPreview currentRoomPreview() const { return roomlistmodel->currentRoomPreview(); }
+    void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
+    void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); }
+
+    void nextRoomWithActivity();
+    void nextRoom();
+    void previousRoom();
+
+    void updateFilterTag(QString tagId)
+    {
+        if (tagId.startsWith("tag:")) {
+            filterType = FilterBy::Tag;
+            filterStr  = tagId.mid(4);
+        } else if (tagId.startsWith("space:")) {
+            filterType = FilterBy::Space;
+            filterStr  = tagId.mid(6);
+        } else {
+            filterType = FilterBy::Nothing;
+            filterStr.clear();
         }
 
-        void updateHiddenTagsAndSpaces();
+        invalidateFilter();
+    }
+
+    void updateHiddenTagsAndSpaces();
 
 signals:
-        void currentRoomChanged();
+    void currentRoomChanged();
 
 private:
-        short int calculateImportance(const QModelIndex &idx) const;
-        RoomlistModel *roomlistmodel;
-        bool sortByImportance = true;
-
-        enum class FilterBy
-        {
-                Tag,
-                Space,
-                Nothing,
-        };
-        QString filterStr   = "";
-        FilterBy filterType = FilterBy::Nothing;
-        QStringList hiddenTags, hiddenSpaces;
+    short int calculateImportance(const QModelIndex &idx) const;
+    RoomlistModel *roomlistmodel;
+    bool sortByImportance = true;
+
+    enum class FilterBy
+    {
+        Tag,
+        Space,
+        Nothing,
+    };
+    QString filterStr   = "";
+    FilterBy filterType = FilterBy::Nothing;
+    QStringList hiddenTags, hiddenSpaces;
 };
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 79c28edfd0b667f1cea1535d7bd8771ce0332550..0e5ce510b1ea80258d676bdfe635a02d5e1f9f57 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -27,10 +27,10 @@
 #include "MatrixClient.h"
 #include "MemberList.h"
 #include "MxcImageProvider.h"
-#include "Olm.h"
 #include "ReadReceiptsModel.h"
 #include "TimelineViewManager.h"
 #include "Utils.h"
+#include "encryption/Olm.h"
 
 Q_DECLARE_METATYPE(QModelIndex)
 
@@ -38,288 +38,285 @@ namespace std {
 inline uint
 qHash(const std::string &key, uint seed = 0)
 {
-        return qHash(QByteArray::fromRawData(key.data(), (int)key.length()), seed);
+    return qHash(QByteArray::fromRawData(key.data(), (int)key.length()), seed);
 }
 }
 
 namespace {
 struct RoomEventType
 {
-        template<class T>
-        qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e)
-        {
-                using mtx::events::EventType;
-                switch (e.type) {
-                case EventType::RoomKeyRequest:
-                        return qml_mtx_events::EventType::KeyRequest;
-                case EventType::Reaction:
-                        return qml_mtx_events::EventType::Reaction;
-                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::RoomCreate;
-                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::RoomGuestAccess;
-                case EventType::RoomHistoryVisibility:
-                        return qml_mtx_events::EventType::RoomHistoryVisibility;
-                case EventType::RoomJoinRules:
-                        return qml_mtx_events::EventType::RoomJoinRules;
-                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:
-                        return qml_mtx_events::EventType::Unsupported;
-                default:
-                        return qml_mtx_events::EventType::UnknownMessage;
-                }
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &)
-        {
-                return qml_mtx_events::EventType::AudioMessage;
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &)
-        {
-                return qml_mtx_events::EventType::EmoteMessage;
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &)
-        {
-                return qml_mtx_events::EventType::FileMessage;
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &)
-        {
-                return qml_mtx_events::EventType::ImageMessage;
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &)
-        {
-                return qml_mtx_events::EventType::NoticeMessage;
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &)
-        {
-                return qml_mtx_events::EventType::TextMessage;
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &)
-        {
-                return qml_mtx_events::EventType::VideoMessage;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationRequest;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationStart;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationMac;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationAccept;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationReady;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationCancel;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationKey;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &)
-        {
-                return qml_mtx_events::EventType::KeyVerificationDone;
-        }
-        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
-        {
-                return qml_mtx_events::EventType::Redacted;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::CallInvite> &)
-        {
-                return qml_mtx_events::EventType::CallInvite;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::CallAnswer> &)
-        {
-                return qml_mtx_events::EventType::CallAnswer;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::CallHangUp> &)
-        {
-                return qml_mtx_events::EventType::CallHangUp;
-        }
-        qml_mtx_events::EventType operator()(
-          const mtx::events::Event<mtx::events::msg::CallCandidates> &)
-        {
-                return qml_mtx_events::EventType::CallCandidates;
-        }
-        // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
-        // ::EventType::LocationMessage; }
+    template<class T>
+    qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e)
+    {
+        using mtx::events::EventType;
+        switch (e.type) {
+        case EventType::RoomKeyRequest:
+            return qml_mtx_events::EventType::KeyRequest;
+        case EventType::Reaction:
+            return qml_mtx_events::EventType::Reaction;
+        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::RoomCreate;
+        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::RoomGuestAccess;
+        case EventType::RoomHistoryVisibility:
+            return qml_mtx_events::EventType::RoomHistoryVisibility;
+        case EventType::RoomJoinRules:
+            return qml_mtx_events::EventType::RoomJoinRules;
+        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:
+            return qml_mtx_events::EventType::Unsupported;
+        default:
+            return qml_mtx_events::EventType::UnknownMessage;
+        }
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &)
+    {
+        return qml_mtx_events::EventType::AudioMessage;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &)
+    {
+        return qml_mtx_events::EventType::EmoteMessage;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &)
+    {
+        return qml_mtx_events::EventType::FileMessage;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &)
+    {
+        return qml_mtx_events::EventType::ImageMessage;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &)
+    {
+        return qml_mtx_events::EventType::NoticeMessage;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &)
+    {
+        return qml_mtx_events::EventType::TextMessage;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &)
+    {
+        return qml_mtx_events::EventType::VideoMessage;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationRequest;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationStart;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationMac;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationAccept;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationReady;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationCancel;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationKey;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &)
+    {
+        return qml_mtx_events::EventType::KeyVerificationDone;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
+    {
+        return qml_mtx_events::EventType::Redacted;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallInvite> &)
+    {
+        return qml_mtx_events::EventType::CallInvite;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallAnswer> &)
+    {
+        return qml_mtx_events::EventType::CallAnswer;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallHangUp> &)
+    {
+        return qml_mtx_events::EventType::CallHangUp;
+    }
+    qml_mtx_events::EventType operator()(
+      const mtx::events::Event<mtx::events::msg::CallCandidates> &)
+    {
+        return qml_mtx_events::EventType::CallCandidates;
+    }
+    // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
+    // ::EventType::LocationMessage; }
 };
 }
 
 qml_mtx_events::EventType
 toRoomEventType(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(RoomEventType{}, event);
+    return std::visit(RoomEventType{}, event);
 }
 
 QString
 toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
-                          event);
+    return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
+                      event);
 }
 
 mtx::events::EventType
 qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
 {
-        switch (t) {
-        // Unsupported event
-        case qml_mtx_events::Unsupported:
-                return mtx::events::EventType::Unsupported;
-
-        /// m.room_key_request
-        case qml_mtx_events::KeyRequest:
-                return mtx::events::EventType::RoomKeyRequest;
-        /// m.reaction:
-        case qml_mtx_events::Reaction:
-                return mtx::events::EventType::Reaction;
-        /// m.room.aliases
-        case qml_mtx_events::Aliases:
-                return mtx::events::EventType::RoomAliases;
-        /// m.room.avatar
-        case qml_mtx_events::Avatar:
-                return mtx::events::EventType::RoomAvatar;
-        /// m.call.invite
-        case qml_mtx_events::CallInvite:
-                return mtx::events::EventType::CallInvite;
-        /// m.call.answer
-        case qml_mtx_events::CallAnswer:
-                return mtx::events::EventType::CallAnswer;
-        /// m.call.hangup
-        case qml_mtx_events::CallHangUp:
-                return mtx::events::EventType::CallHangUp;
-        /// m.call.candidates
-        case qml_mtx_events::CallCandidates:
-                return mtx::events::EventType::CallCandidates;
-        /// m.room.canonical_alias
-        case qml_mtx_events::CanonicalAlias:
-                return mtx::events::EventType::RoomCanonicalAlias;
-        /// m.room.create
-        case qml_mtx_events::RoomCreate:
-                return mtx::events::EventType::RoomCreate;
-        /// m.room.encrypted.
-        case qml_mtx_events::Encrypted:
-                return mtx::events::EventType::RoomEncrypted;
-        /// m.room.encryption.
-        case qml_mtx_events::Encryption:
-                return mtx::events::EventType::RoomEncryption;
-        /// m.room.guest_access
-        case qml_mtx_events::RoomGuestAccess:
-                return mtx::events::EventType::RoomGuestAccess;
-        /// m.room.history_visibility
-        case qml_mtx_events::RoomHistoryVisibility:
-                return mtx::events::EventType::RoomHistoryVisibility;
-        /// m.room.join_rules
-        case qml_mtx_events::RoomJoinRules:
-                return mtx::events::EventType::RoomJoinRules;
-        /// m.room.member
-        case qml_mtx_events::Member:
-                return mtx::events::EventType::RoomMember;
-        /// m.room.name
-        case qml_mtx_events::Name:
-                return mtx::events::EventType::RoomName;
-        /// m.room.power_levels
-        case qml_mtx_events::PowerLevels:
-                return mtx::events::EventType::RoomPowerLevels;
-        /// m.room.tombstone
-        case qml_mtx_events::Tombstone:
-                return mtx::events::EventType::RoomTombstone;
-        /// m.room.topic
-        case qml_mtx_events::Topic:
-                return mtx::events::EventType::RoomTopic;
-        /// m.room.redaction
-        case qml_mtx_events::Redaction:
-                return mtx::events::EventType::RoomRedaction;
-        /// m.room.pinned_events
-        case qml_mtx_events::PinnedEvents:
-                return mtx::events::EventType::RoomPinnedEvents;
-        // m.sticker
-        case qml_mtx_events::Sticker:
-                return mtx::events::EventType::Sticker;
-        // m.tag
-        case qml_mtx_events::Tag:
-                return mtx::events::EventType::Tag;
-        /// m.room.message
-        case qml_mtx_events::AudioMessage:
-        case qml_mtx_events::EmoteMessage:
-        case qml_mtx_events::FileMessage:
-        case qml_mtx_events::ImageMessage:
-        case qml_mtx_events::LocationMessage:
-        case qml_mtx_events::NoticeMessage:
-        case qml_mtx_events::TextMessage:
-        case qml_mtx_events::VideoMessage:
-        case qml_mtx_events::Redacted:
-        case qml_mtx_events::UnknownMessage:
-        case qml_mtx_events::KeyVerificationRequest:
-        case qml_mtx_events::KeyVerificationStart:
-        case qml_mtx_events::KeyVerificationMac:
-        case qml_mtx_events::KeyVerificationAccept:
-        case qml_mtx_events::KeyVerificationCancel:
-        case qml_mtx_events::KeyVerificationKey:
-        case qml_mtx_events::KeyVerificationDone:
-        case qml_mtx_events::KeyVerificationReady:
-                return mtx::events::EventType::RoomMessage;
-                //! m.image_pack, currently im.ponies.room_emotes
-        case qml_mtx_events::ImagePackInRoom:
-                return mtx::events::EventType::ImagePackInRoom;
-        //! m.image_pack, currently im.ponies.user_emotes
-        case qml_mtx_events::ImagePackInAccountData:
-                return mtx::events::EventType::ImagePackInAccountData;
-        //! m.image_pack.rooms, currently im.ponies.emote_rooms
-        case qml_mtx_events::ImagePackRooms:
-                return mtx::events::EventType::ImagePackRooms;
-        default:
-                return mtx::events::EventType::Unsupported;
-        };
+    switch (t) {
+    // Unsupported event
+    case qml_mtx_events::Unsupported:
+        return mtx::events::EventType::Unsupported;
+
+    /// m.room_key_request
+    case qml_mtx_events::KeyRequest:
+        return mtx::events::EventType::RoomKeyRequest;
+    /// m.reaction:
+    case qml_mtx_events::Reaction:
+        return mtx::events::EventType::Reaction;
+    /// m.room.aliases
+    case qml_mtx_events::Aliases:
+        return mtx::events::EventType::RoomAliases;
+    /// m.room.avatar
+    case qml_mtx_events::Avatar:
+        return mtx::events::EventType::RoomAvatar;
+    /// m.call.invite
+    case qml_mtx_events::CallInvite:
+        return mtx::events::EventType::CallInvite;
+    /// m.call.answer
+    case qml_mtx_events::CallAnswer:
+        return mtx::events::EventType::CallAnswer;
+    /// m.call.hangup
+    case qml_mtx_events::CallHangUp:
+        return mtx::events::EventType::CallHangUp;
+    /// m.call.candidates
+    case qml_mtx_events::CallCandidates:
+        return mtx::events::EventType::CallCandidates;
+    /// m.room.canonical_alias
+    case qml_mtx_events::CanonicalAlias:
+        return mtx::events::EventType::RoomCanonicalAlias;
+    /// m.room.create
+    case qml_mtx_events::RoomCreate:
+        return mtx::events::EventType::RoomCreate;
+    /// m.room.encrypted.
+    case qml_mtx_events::Encrypted:
+        return mtx::events::EventType::RoomEncrypted;
+    /// m.room.encryption.
+    case qml_mtx_events::Encryption:
+        return mtx::events::EventType::RoomEncryption;
+    /// m.room.guest_access
+    case qml_mtx_events::RoomGuestAccess:
+        return mtx::events::EventType::RoomGuestAccess;
+    /// m.room.history_visibility
+    case qml_mtx_events::RoomHistoryVisibility:
+        return mtx::events::EventType::RoomHistoryVisibility;
+    /// m.room.join_rules
+    case qml_mtx_events::RoomJoinRules:
+        return mtx::events::EventType::RoomJoinRules;
+    /// m.room.member
+    case qml_mtx_events::Member:
+        return mtx::events::EventType::RoomMember;
+    /// m.room.name
+    case qml_mtx_events::Name:
+        return mtx::events::EventType::RoomName;
+    /// m.room.power_levels
+    case qml_mtx_events::PowerLevels:
+        return mtx::events::EventType::RoomPowerLevels;
+    /// m.room.tombstone
+    case qml_mtx_events::Tombstone:
+        return mtx::events::EventType::RoomTombstone;
+    /// m.room.topic
+    case qml_mtx_events::Topic:
+        return mtx::events::EventType::RoomTopic;
+    /// m.room.redaction
+    case qml_mtx_events::Redaction:
+        return mtx::events::EventType::RoomRedaction;
+    /// m.room.pinned_events
+    case qml_mtx_events::PinnedEvents:
+        return mtx::events::EventType::RoomPinnedEvents;
+    // m.sticker
+    case qml_mtx_events::Sticker:
+        return mtx::events::EventType::Sticker;
+    // m.tag
+    case qml_mtx_events::Tag:
+        return mtx::events::EventType::Tag;
+    /// m.room.message
+    case qml_mtx_events::AudioMessage:
+    case qml_mtx_events::EmoteMessage:
+    case qml_mtx_events::FileMessage:
+    case qml_mtx_events::ImageMessage:
+    case qml_mtx_events::LocationMessage:
+    case qml_mtx_events::NoticeMessage:
+    case qml_mtx_events::TextMessage:
+    case qml_mtx_events::VideoMessage:
+    case qml_mtx_events::Redacted:
+    case qml_mtx_events::UnknownMessage:
+    case qml_mtx_events::KeyVerificationRequest:
+    case qml_mtx_events::KeyVerificationStart:
+    case qml_mtx_events::KeyVerificationMac:
+    case qml_mtx_events::KeyVerificationAccept:
+    case qml_mtx_events::KeyVerificationCancel:
+    case qml_mtx_events::KeyVerificationKey:
+    case qml_mtx_events::KeyVerificationDone:
+    case qml_mtx_events::KeyVerificationReady:
+        return mtx::events::EventType::RoomMessage;
+        //! m.image_pack, currently im.ponies.room_emotes
+    case qml_mtx_events::ImagePackInRoom:
+        return mtx::events::EventType::ImagePackInRoom;
+    //! m.image_pack, currently im.ponies.user_emotes
+    case qml_mtx_events::ImagePackInAccountData:
+        return mtx::events::EventType::ImagePackInAccountData;
+    //! m.image_pack.rooms, currently im.ponies.emote_rooms
+    case qml_mtx_events::ImagePackRooms:
+        return mtx::events::EventType::ImagePackRooms;
+    default:
+        return mtx::events::EventType::Unsupported;
+    };
 }
 
 TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
@@ -329,564 +326,549 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
   , manager_(manager)
   , permissions_{room_id}
 {
-        lastMessage_.timestamp = 0;
-
-        if (auto create =
-              cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString()))
-                this->isSpace_ = create->content.type == mtx::events::state::room_type::space;
-        this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
-
-        // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it
-        // needs to be
-        connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged);
-
-        connect(
-          this,
-          &TimelineModel::redactionFailed,
-          this,
-          [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); },
-          Qt::QueuedConnection);
-
-        connect(this,
-                &TimelineModel::newMessageToSend,
-                this,
-                &TimelineModel::addPendingMessage,
-                Qt::QueuedConnection);
-        connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending);
-
-        connect(
-          &events,
-          &EventStore::dataChanged,
-          this,
-          [this](int from, int to) {
-                  relatedEventCacheBuster++;
-                  nhlog::ui()->debug(
-                    "data changed {} to {}", events.size() - to - 1, events.size() - from - 1);
-                  emit dataChanged(index(events.size() - to - 1, 0),
-                                   index(events.size() - from - 1, 0));
-          },
-          Qt::QueuedConnection);
-
-        connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) {
-                int first = events.size() - to;
-                int last  = events.size() - from;
-                if (from >= events.size()) {
-                        int batch_size = to - from;
-                        first += batch_size;
-                        last += batch_size;
-                } else {
-                        first -= 1;
-                        last -= 1;
-                }
-                nhlog::ui()->debug("begin insert from {} to {}", first, last);
-                beginInsertRows(QModelIndex(), first, last);
-        });
-        connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); });
-        connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); });
-        connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); });
-        connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
-        connect(
-          &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
-        connect(&events,
-                &EventStore::startDMVerification,
-                this,
-                [this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) {
-                        ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this);
-                });
-        connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
-                this->updateFlowEventId(event_id);
-        });
-
-        // When a message is sent, check if the current edit/reply relates to that message,
-        // and update the event_id so that it points to the sent message and not the pending one.
-        connect(&events,
-                &EventStore::messageSent,
-                this,
-                [this](std::string txn_id, std::string event_id) {
-                        if (edit_.toStdString() == txn_id) {
-                                edit_ = QString::fromStdString(event_id);
-                                emit editChanged(edit_);
-                        }
-                        if (reply_.toStdString() == txn_id) {
-                                reply_ = QString::fromStdString(event_id);
-                                emit replyChanged(reply_);
-                        }
-                });
-
-        connect(manager_,
-                &TimelineViewManager::initialSyncChanged,
-                &events,
-                &EventStore::enableKeyRequests);
-
-        connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged);
-        connect(
-          this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged);
-        connect(cache::client(),
-                &Cache::verificationStatusChanged,
-                this,
-                &TimelineModel::trustlevelChanged);
-
-        showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
+    lastMessage_.timestamp = 0;
+
+    if (auto create =
+          cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString()))
+        this->isSpace_ = create->content.type == mtx::events::state::room_type::space;
+    this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
+
+    // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it
+    // needs to be
+    connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged);
+
+    connect(
+      this,
+      &TimelineModel::redactionFailed,
+      this,
+      [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); },
+      Qt::QueuedConnection);
+
+    connect(this,
+            &TimelineModel::newMessageToSend,
+            this,
+            &TimelineModel::addPendingMessage,
+            Qt::QueuedConnection);
+    connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending);
+
+    connect(&events, &EventStore::dataChanged, this, [this](int from, int to) {
+        relatedEventCacheBuster++;
+        nhlog::ui()->debug(
+          "data changed {} to {}", events.size() - to - 1, events.size() - from - 1);
+        emit dataChanged(index(events.size() - to - 1, 0), index(events.size() - from - 1, 0));
+    });
+
+    connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) {
+        int first = events.size() - to;
+        int last  = events.size() - from;
+        if (from >= events.size()) {
+            int batch_size = to - from;
+            first += batch_size;
+            last += batch_size;
+        } else {
+            first -= 1;
+            last -= 1;
+        }
+        nhlog::ui()->debug("begin insert from {} to {}", first, last);
+        beginInsertRows(QModelIndex(), first, last);
+    });
+    connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); });
+    connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); });
+    connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); });
+    connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
+    connect(&events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
+    connect(&events,
+            &EventStore::startDMVerification,
+            this,
+            [this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) {
+                ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this);
+            });
+    connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
+        this->updateFlowEventId(event_id);
+    });
+
+    // When a message is sent, check if the current edit/reply relates to that message,
+    // and update the event_id so that it points to the sent message and not the pending one.
+    connect(
+      &events, &EventStore::messageSent, this, [this](std::string txn_id, std::string event_id) {
+          if (edit_.toStdString() == txn_id) {
+              edit_ = QString::fromStdString(event_id);
+              emit editChanged(edit_);
+          }
+          if (reply_.toStdString() == txn_id) {
+              reply_ = QString::fromStdString(event_id);
+              emit replyChanged(reply_);
+          }
+      });
+
+    connect(
+      manager_, &TimelineViewManager::initialSyncChanged, &events, &EventStore::enableKeyRequests);
+
+    connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged);
+    connect(this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged);
+    connect(
+      cache::client(), &Cache::verificationStatusChanged, this, &TimelineModel::trustlevelChanged);
+
+    showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
 }
 
 QHash<int, QByteArray>
 TimelineModel::roleNames() const
 {
-        return {
-          {Type, "type"},
-          {TypeString, "typeString"},
-          {IsOnlyEmoji, "isOnlyEmoji"},
-          {Body, "body"},
-          {FormattedBody, "formattedBody"},
-          {PreviousMessageUserId, "previousMessageUserId"},
-          {IsSender, "isSender"},
-          {UserId, "userId"},
-          {UserName, "userName"},
-          {PreviousMessageDay, "previousMessageDay"},
-          {Day, "day"},
-          {Timestamp, "timestamp"},
-          {Url, "url"},
-          {ThumbnailUrl, "thumbnailUrl"},
-          {Blurhash, "blurhash"},
-          {Filename, "filename"},
-          {Filesize, "filesize"},
-          {MimeType, "mimetype"},
-          {OriginalHeight, "originalHeight"},
-          {OriginalWidth, "originalWidth"},
-          {ProportionalHeight, "proportionalHeight"},
-          {EventId, "eventId"},
-          {State, "status"},
-          {IsEdited, "isEdited"},
-          {IsEditable, "isEditable"},
-          {IsEncrypted, "isEncrypted"},
-          {Trustlevel, "trustlevel"},
-          {EncryptionError, "encryptionError"},
-          {ReplyTo, "replyTo"},
-          {Reactions, "reactions"},
-          {RoomId, "roomId"},
-          {RoomName, "roomName"},
-          {RoomTopic, "roomTopic"},
-          {CallType, "callType"},
-          {Dump, "dump"},
-          {RelatedEventCacheBuster, "relatedEventCacheBuster"},
-        };
+    return {
+      {Type, "type"},
+      {TypeString, "typeString"},
+      {IsOnlyEmoji, "isOnlyEmoji"},
+      {Body, "body"},
+      {FormattedBody, "formattedBody"},
+      {PreviousMessageUserId, "previousMessageUserId"},
+      {IsSender, "isSender"},
+      {UserId, "userId"},
+      {UserName, "userName"},
+      {PreviousMessageDay, "previousMessageDay"},
+      {Day, "day"},
+      {Timestamp, "timestamp"},
+      {Url, "url"},
+      {ThumbnailUrl, "thumbnailUrl"},
+      {Blurhash, "blurhash"},
+      {Filename, "filename"},
+      {Filesize, "filesize"},
+      {MimeType, "mimetype"},
+      {OriginalHeight, "originalHeight"},
+      {OriginalWidth, "originalWidth"},
+      {ProportionalHeight, "proportionalHeight"},
+      {EventId, "eventId"},
+      {State, "status"},
+      {IsEdited, "isEdited"},
+      {IsEditable, "isEditable"},
+      {IsEncrypted, "isEncrypted"},
+      {Trustlevel, "trustlevel"},
+      {EncryptionError, "encryptionError"},
+      {ReplyTo, "replyTo"},
+      {Reactions, "reactions"},
+      {RoomId, "roomId"},
+      {RoomName, "roomName"},
+      {RoomTopic, "roomTopic"},
+      {CallType, "callType"},
+      {Dump, "dump"},
+      {RelatedEventCacheBuster, "relatedEventCacheBuster"},
+    };
 }
 int
 TimelineModel::rowCount(const QModelIndex &parent) const
 {
-        Q_UNUSED(parent);
-        return this->events.size();
+    Q_UNUSED(parent);
+    return this->events.size();
 }
 
 QVariantMap
 TimelineModel::getDump(QString eventId, QString relatedTo) const
 {
-        if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString()))
-                return data(*event, Dump).toMap();
-        return {};
+    if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString()))
+        return data(*event, Dump).toMap();
+    return {};
 }
 
 QVariant
 TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const
 {
-        using namespace mtx::accessors;
-        namespace acc = mtx::accessors;
-
-        switch (role) {
-        case IsSender:
-                return QVariant(acc::sender(event) == http::client()->user_id().to_string());
-        case UserId:
-                return QVariant(QString::fromStdString(acc::sender(event)));
-        case UserName:
-                return QVariant(displayName(QString::fromStdString(acc::sender(event))));
-
-        case Day: {
-                QDateTime prevDate = origin_server_ts(event);
-                prevDate.setTime(QTime());
-                return QVariant(prevDate.toMSecsSinceEpoch());
-        }
-        case Timestamp:
-                return QVariant(origin_server_ts(event));
-        case Type:
-                return QVariant(toRoomEventType(event));
-        case TypeString:
-                return QVariant(toRoomEventTypeString(event));
-        case IsOnlyEmoji: {
-                QString qBody = QString::fromStdString(body(event));
-
-                QVector<uint> utf32_string = qBody.toUcs4();
-                int emojiCount             = 0;
-
-                for (auto &code : utf32_string) {
-                        if (utils::codepointIsEmoji(code)) {
-                                emojiCount++;
-                        } else {
-                                return QVariant(0);
-                        }
-                }
-
-                return QVariant(emojiCount);
-        }
-        case Body:
-                return QVariant(
-                  utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped()));
-        case FormattedBody: {
-                const static QRegularExpression replyFallback(
-                  "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
-
-                auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
-
-                bool isReply = utils::isReply(event);
-
-                auto formattedBody_ = QString::fromStdString(formatted_body(event));
-                if (formattedBody_.isEmpty()) {
-                        auto body_ = QString::fromStdString(body(event));
-
-                        if (isReply) {
-                                while (body_.startsWith("> "))
-                                        body_ = body_.right(body_.size() - body_.indexOf('\n') - 1);
-                                if (body_.startsWith('\n'))
-                                        body_ = body_.right(body_.size() - 1);
-                        }
-                        formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>");
-                } else {
-                        if (isReply)
-                                formattedBody_ = formattedBody_.remove(replyFallback);
-                }
-
-                // TODO(Nico): Don't parse html with a regex
-                const static QRegularExpression matchImgUri(
-                  "(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)");
-                formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3");
-                // Same regex but for single quotes around the src
-                const static QRegularExpression matchImgUri2(
-                  "(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)");
-                formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3");
-                const static QRegularExpression matchEmoticonHeight(
-                  "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)");
-                formattedBody_.replace(matchEmoticonHeight,
-                                       QString("\\1 height=\"%1\"\\3").arg(ascent));
-
-                return QVariant(utils::replaceEmoji(
-                  utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
-        }
-        case Url:
-                return QVariant(QString::fromStdString(url(event)));
-        case ThumbnailUrl:
-                return QVariant(QString::fromStdString(thumbnail_url(event)));
-        case Blurhash:
-                return QVariant(QString::fromStdString(blurhash(event)));
-        case Filename:
-                return QVariant(QString::fromStdString(filename(event)));
-        case Filesize:
-                return QVariant(utils::humanReadableFileSize(filesize(event)));
-        case MimeType:
-                return QVariant(QString::fromStdString(mimetype(event)));
-        case OriginalHeight:
-                return QVariant(qulonglong{media_height(event)});
-        case OriginalWidth:
-                return QVariant(qulonglong{media_width(event)});
-        case ProportionalHeight: {
-                auto w = media_width(event);
-                if (w == 0)
-                        w = 1;
-
-                double prop = media_height(event) / (double)w;
-
-                return QVariant(prop > 0 ? prop : 1.);
-        }
-        case EventId: {
-                if (auto replaces = relations(event).replaces())
-                        return QVariant(QString::fromStdString(replaces.value()));
-                else
-                        return QVariant(QString::fromStdString(event_id(event)));
-        }
-        case State: {
-                auto id             = QString::fromStdString(event_id(event));
-                auto containsOthers = [](const auto &vec) {
-                        for (const auto &e : vec)
-                                if (e.second != http::client()->user_id().to_string())
-                                        return true;
-                        return false;
-                };
-
-                // only show read receipts for messages not from us
-                if (acc::sender(event) != http::client()->user_id().to_string())
-                        return qml_mtx_events::Empty;
-                else if (!id.isEmpty() && id[0] == "m")
-                        return qml_mtx_events::Sent;
-                else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_)))
-                        return qml_mtx_events::Read;
-                else
-                        return qml_mtx_events::Received;
-        }
-        case IsEdited:
-                return QVariant(relations(event).replaces().has_value());
-        case IsEditable:
-                return QVariant(!is_state_event(event) && mtx::accessors::sender(event) ==
-                                                            http::client()->user_id().to_string());
-        case IsEncrypted: {
-                auto id              = event_id(event);
-                auto encrypted_event = events.get(id, "", false);
-                return encrypted_event &&
-                       std::holds_alternative<
-                         mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                         *encrypted_event);
-        }
-
-        case Trustlevel: {
-                auto id              = event_id(event);
-                auto encrypted_event = events.get(id, "", false);
-                if (encrypted_event) {
-                        if (auto encrypted =
-                              std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                                &*encrypted_event)) {
-                                return olm::calculate_trust(encrypted->sender,
-                                                            encrypted->content.sender_key);
-                        }
-                }
-                return crypto::Trust::Unverified;
-        }
-
-        case EncryptionError:
-                return events.decryptionError(event_id(event));
+    using namespace mtx::accessors;
+    namespace acc = mtx::accessors;
+
+    switch (role) {
+    case IsSender:
+        return QVariant(acc::sender(event) == http::client()->user_id().to_string());
+    case UserId:
+        return QVariant(QString::fromStdString(acc::sender(event)));
+    case UserName:
+        return QVariant(displayName(QString::fromStdString(acc::sender(event))));
+
+    case Day: {
+        QDateTime prevDate = origin_server_ts(event);
+        prevDate.setTime(QTime());
+        return QVariant(prevDate.toMSecsSinceEpoch());
+    }
+    case Timestamp:
+        return QVariant(origin_server_ts(event));
+    case Type:
+        return QVariant(toRoomEventType(event));
+    case TypeString:
+        return QVariant(toRoomEventTypeString(event));
+    case IsOnlyEmoji: {
+        QString qBody = QString::fromStdString(body(event));
+
+        QVector<uint> utf32_string = qBody.toUcs4();
+        int emojiCount             = 0;
+
+        for (auto &code : utf32_string) {
+            if (utils::codepointIsEmoji(code)) {
+                emojiCount++;
+            } else {
+                return QVariant(0);
+            }
+        }
+
+        return QVariant(emojiCount);
+    }
+    case Body:
+        return QVariant(utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped()));
+    case FormattedBody: {
+        const static QRegularExpression replyFallback(
+          "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
+
+        auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
+
+        bool isReply = utils::isReply(event);
+
+        auto formattedBody_ = QString::fromStdString(formatted_body(event));
+        if (formattedBody_.isEmpty()) {
+            auto body_ = QString::fromStdString(body(event));
+
+            if (isReply) {
+                while (body_.startsWith("> "))
+                    body_ = body_.right(body_.size() - body_.indexOf('\n') - 1);
+                if (body_.startsWith('\n'))
+                    body_ = body_.right(body_.size() - 1);
+            }
+            formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>");
+        } else {
+            if (isReply)
+                formattedBody_ = formattedBody_.remove(replyFallback);
+        }
+
+        // TODO(Nico): Don't parse html with a regex
+        const static QRegularExpression matchImgUri("(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)");
+        formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3");
+        // Same regex but for single quotes around the src
+        const static QRegularExpression matchImgUri2("(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)");
+        formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3");
+        const static QRegularExpression matchEmoticonHeight(
+          "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)");
+        formattedBody_.replace(matchEmoticonHeight, QString("\\1 height=\"%1\"\\3").arg(ascent));
+
+        return QVariant(
+          utils::replaceEmoji(utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
+    }
+    case Url:
+        return QVariant(QString::fromStdString(url(event)));
+    case ThumbnailUrl:
+        return QVariant(QString::fromStdString(thumbnail_url(event)));
+    case Blurhash:
+        return QVariant(QString::fromStdString(blurhash(event)));
+    case Filename:
+        return QVariant(QString::fromStdString(filename(event)));
+    case Filesize:
+        return QVariant(utils::humanReadableFileSize(filesize(event)));
+    case MimeType:
+        return QVariant(QString::fromStdString(mimetype(event)));
+    case OriginalHeight:
+        return QVariant(qulonglong{media_height(event)});
+    case OriginalWidth:
+        return QVariant(qulonglong{media_width(event)});
+    case ProportionalHeight: {
+        auto w = media_width(event);
+        if (w == 0)
+            w = 1;
+
+        double prop = media_height(event) / (double)w;
+
+        return QVariant(prop > 0 ? prop : 1.);
+    }
+    case EventId: {
+        if (auto replaces = relations(event).replaces())
+            return QVariant(QString::fromStdString(replaces.value()));
+        else
+            return QVariant(QString::fromStdString(event_id(event)));
+    }
+    case State: {
+        auto id             = QString::fromStdString(event_id(event));
+        auto containsOthers = [](const auto &vec) {
+            for (const auto &e : vec)
+                if (e.second != http::client()->user_id().to_string())
+                    return true;
+            return false;
+        };
 
-        case ReplyTo:
-                return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
-        case Reactions: {
-                auto id = relations(event).replaces().value_or(event_id(event));
-                return QVariant::fromValue(events.reactions(id));
-        }
-        case RoomId:
-                return QVariant(room_id_);
-        case RoomName:
-                return QVariant(
-                  utils::replaceEmoji(QString::fromStdString(room_name(event)).toHtmlEscaped()));
-        case RoomTopic:
-                return QVariant(utils::replaceEmoji(
-                  utils::linkifyMessage(QString::fromStdString(room_topic(event))
-                                          .toHtmlEscaped()
-                                          .replace("\n", "<br>"))));
-        case CallType:
-                return QVariant(QString::fromStdString(call_type(event)));
-        case Dump: {
-                QVariantMap m;
-                auto names = roleNames();
-
-                m.insert(names[Type], data(event, static_cast<int>(Type)));
-                m.insert(names[TypeString], data(event, static_cast<int>(TypeString)));
-                m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji)));
-                m.insert(names[Body], data(event, static_cast<int>(Body)));
-                m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody)));
-                m.insert(names[IsSender], data(event, static_cast<int>(IsSender)));
-                m.insert(names[UserId], data(event, static_cast<int>(UserId)));
-                m.insert(names[UserName], data(event, static_cast<int>(UserName)));
-                m.insert(names[Day], data(event, static_cast<int>(Day)));
-                m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp)));
-                m.insert(names[Url], data(event, static_cast<int>(Url)));
-                m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl)));
-                m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash)));
-                m.insert(names[Filename], data(event, static_cast<int>(Filename)));
-                m.insert(names[Filesize], data(event, static_cast<int>(Filesize)));
-                m.insert(names[MimeType], data(event, static_cast<int>(MimeType)));
-                m.insert(names[OriginalHeight], data(event, static_cast<int>(OriginalHeight)));
-                m.insert(names[OriginalWidth], data(event, static_cast<int>(OriginalWidth)));
-                m.insert(names[ProportionalHeight],
-                         data(event, static_cast<int>(ProportionalHeight)));
-                m.insert(names[EventId], data(event, static_cast<int>(EventId)));
-                m.insert(names[State], data(event, static_cast<int>(State)));
-                m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited)));
-                m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable)));
-                m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
-                m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
-                m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
-                m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
-                m.insert(names[CallType], data(event, static_cast<int>(CallType)));
-                m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError)));
-
-                return QVariant(m);
-        }
-        case RelatedEventCacheBuster:
-                return relatedEventCacheBuster;
-        default:
-                return QVariant();
-        }
+        // only show read receipts for messages not from us
+        if (acc::sender(event) != http::client()->user_id().to_string())
+            return qml_mtx_events::Empty;
+        else if (!id.isEmpty() && id[0] == "m")
+            return qml_mtx_events::Sent;
+        else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_)))
+            return qml_mtx_events::Read;
+        else
+            return qml_mtx_events::Received;
+    }
+    case IsEdited:
+        return QVariant(relations(event).replaces().has_value());
+    case IsEditable:
+        return QVariant(!is_state_event(event) &&
+                        mtx::accessors::sender(event) == http::client()->user_id().to_string());
+    case IsEncrypted: {
+        auto id              = event_id(event);
+        auto encrypted_event = events.get(id, "", false);
+        return encrypted_event &&
+               std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+                 *encrypted_event);
+    }
+
+    case Trustlevel: {
+        auto id              = event_id(event);
+        auto encrypted_event = events.get(id, "", false);
+        if (encrypted_event) {
+            if (auto encrypted =
+                  std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+                    &*encrypted_event)) {
+                return olm::calculate_trust(
+                  encrypted->sender,
+                  MegolmSessionIndex(room_id_.toStdString(), encrypted->content));
+            }
+        }
+        return crypto::Trust::Unverified;
+    }
+
+    case EncryptionError:
+        return events.decryptionError(event_id(event));
+
+    case ReplyTo:
+        return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
+    case Reactions: {
+        auto id = relations(event).replaces().value_or(event_id(event));
+        return QVariant::fromValue(events.reactions(id));
+    }
+    case RoomId:
+        return QVariant(room_id_);
+    case RoomName:
+        return QVariant(
+          utils::replaceEmoji(QString::fromStdString(room_name(event)).toHtmlEscaped()));
+    case RoomTopic:
+        return QVariant(utils::replaceEmoji(utils::linkifyMessage(
+          QString::fromStdString(room_topic(event)).toHtmlEscaped().replace("\n", "<br>"))));
+    case CallType:
+        return QVariant(QString::fromStdString(call_type(event)));
+    case Dump: {
+        QVariantMap m;
+        auto names = roleNames();
+
+        m.insert(names[Type], data(event, static_cast<int>(Type)));
+        m.insert(names[TypeString], data(event, static_cast<int>(TypeString)));
+        m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji)));
+        m.insert(names[Body], data(event, static_cast<int>(Body)));
+        m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody)));
+        m.insert(names[IsSender], data(event, static_cast<int>(IsSender)));
+        m.insert(names[UserId], data(event, static_cast<int>(UserId)));
+        m.insert(names[UserName], data(event, static_cast<int>(UserName)));
+        m.insert(names[Day], data(event, static_cast<int>(Day)));
+        m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp)));
+        m.insert(names[Url], data(event, static_cast<int>(Url)));
+        m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl)));
+        m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash)));
+        m.insert(names[Filename], data(event, static_cast<int>(Filename)));
+        m.insert(names[Filesize], data(event, static_cast<int>(Filesize)));
+        m.insert(names[MimeType], data(event, static_cast<int>(MimeType)));
+        m.insert(names[OriginalHeight], data(event, static_cast<int>(OriginalHeight)));
+        m.insert(names[OriginalWidth], data(event, static_cast<int>(OriginalWidth)));
+        m.insert(names[ProportionalHeight], data(event, static_cast<int>(ProportionalHeight)));
+        m.insert(names[EventId], data(event, static_cast<int>(EventId)));
+        m.insert(names[State], data(event, static_cast<int>(State)));
+        m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited)));
+        m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable)));
+        m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
+        m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
+        m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
+        m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
+        m.insert(names[CallType], data(event, static_cast<int>(CallType)));
+        m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError)));
+
+        return QVariant(m);
+    }
+    case RelatedEventCacheBuster:
+        return relatedEventCacheBuster;
+    default:
+        return QVariant();
+    }
 }
 
 QVariant
 TimelineModel::data(const QModelIndex &index, int role) const
 {
-        using namespace mtx::accessors;
-        namespace acc = mtx::accessors;
-        if (index.row() < 0 && index.row() >= rowCount())
-                return QVariant();
+    using namespace mtx::accessors;
+    namespace acc = mtx::accessors;
+    if (index.row() < 0 && index.row() >= rowCount())
+        return QVariant();
 
-        auto event = events.get(rowCount() - index.row() - 1);
+    // HACK(Nico): fetchMore likes to break with dynamically sized delegates and reuseItems
+    if (index.row() + 1 == rowCount() && !m_paginationInProgress)
+        const_cast<TimelineModel *>(this)->fetchMore(index);
 
-        if (!event)
-                return "";
-
-        if (role == PreviousMessageDay || role == PreviousMessageUserId) {
-                int prevIdx = rowCount() - index.row() - 2;
-                if (prevIdx < 0)
-                        return QVariant();
-                auto tempEv = events.get(prevIdx);
-                if (!tempEv)
-                        return QVariant();
-                if (role == PreviousMessageUserId)
-                        return data(*tempEv, UserId);
-                else
-                        return data(*tempEv, Day);
-        }
+    auto event = events.get(rowCount() - index.row() - 1);
 
-        return data(*event, role);
+    if (!event)
+        return "";
+
+    if (role == PreviousMessageDay || role == PreviousMessageUserId) {
+        int prevIdx = rowCount() - index.row() - 2;
+        if (prevIdx < 0)
+            return QVariant();
+        auto tempEv = events.get(prevIdx);
+        if (!tempEv)
+            return QVariant();
+        if (role == PreviousMessageUserId)
+            return data(*tempEv, UserId);
+        else
+            return data(*tempEv, Day);
+    }
+
+    return data(*event, role);
 }
 
 QVariant
 TimelineModel::dataById(QString id, int role, QString relatedTo)
 {
-        if (auto event = events.get(id.toStdString(), relatedTo.toStdString()))
-                return data(*event, role);
-        return QVariant();
+    if (auto event = events.get(id.toStdString(), relatedTo.toStdString()))
+        return data(*event, role);
+    return QVariant();
 }
 
 bool
 TimelineModel::canFetchMore(const QModelIndex &) const
 {
-        if (!events.size())
-                return true;
-        if (auto first = events.get(0);
-            first &&
-            !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first))
-                return true;
-        else
+    if (!events.size())
+        return true;
+    if (auto first = events.get(0);
+        first &&
+        !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first))
+        return true;
+    else
 
-                return false;
+        return false;
 }
 
 void
 TimelineModel::setPaginationInProgress(const bool paginationInProgress)
 {
-        if (m_paginationInProgress == paginationInProgress) {
-                return;
-        }
+    if (m_paginationInProgress == paginationInProgress) {
+        return;
+    }
 
-        m_paginationInProgress = paginationInProgress;
-        emit paginationInProgressChanged(m_paginationInProgress);
+    m_paginationInProgress = paginationInProgress;
+    emit paginationInProgressChanged(m_paginationInProgress);
 }
 
 void
 TimelineModel::fetchMore(const QModelIndex &)
 {
-        if (m_paginationInProgress) {
-                nhlog::ui()->warn("Already loading older messages");
-                return;
-        }
+    if (m_paginationInProgress) {
+        nhlog::ui()->warn("Already loading older messages");
+        return;
+    }
 
-        setPaginationInProgress(true);
+    setPaginationInProgress(true);
 
-        events.fetchMore();
+    events.fetchMore();
 }
 
 void
 TimelineModel::sync(const mtx::responses::JoinedRoom &room)
 {
-        this->syncState(room.state);
-        this->addEvents(room.timeline);
+    this->syncState(room.state);
+    this->addEvents(room.timeline);
 
-        if (room.unread_notifications.highlight_count != highlight_count ||
-            room.unread_notifications.notification_count != notification_count) {
-                notification_count = room.unread_notifications.notification_count;
-                highlight_count    = room.unread_notifications.highlight_count;
-                emit notificationsChanged();
-        }
+    if (room.unread_notifications.highlight_count != highlight_count ||
+        room.unread_notifications.notification_count != notification_count) {
+        notification_count = room.unread_notifications.notification_count;
+        highlight_count    = room.unread_notifications.highlight_count;
+        emit notificationsChanged();
+    }
 }
 
 void
 TimelineModel::syncState(const mtx::responses::State &s)
 {
-        using namespace mtx::events;
-
-        for (const auto &e : s.events) {
-                if (std::holds_alternative<StateEvent<state::Avatar>>(e))
-                        emit roomAvatarUrlChanged();
-                else if (std::holds_alternative<StateEvent<state::Name>>(e))
-                        emit roomNameChanged();
-                else if (std::holds_alternative<StateEvent<state::Topic>>(e))
-                        emit roomTopicChanged();
-                else if (std::holds_alternative<StateEvent<state::Topic>>(e)) {
-                        permissions_.invalidate();
-                        emit permissionsChanged();
-                } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
-                        emit roomAvatarUrlChanged();
-                        emit roomNameChanged();
-                        emit roomMemberCountChanged();
-                } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) {
-                        this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
-                        emit encryptionChanged();
-                }
-        }
+    using namespace mtx::events;
+
+    for (const auto &e : s.events) {
+        if (std::holds_alternative<StateEvent<state::Avatar>>(e))
+            emit roomAvatarUrlChanged();
+        else if (std::holds_alternative<StateEvent<state::Name>>(e))
+            emit roomNameChanged();
+        else if (std::holds_alternative<StateEvent<state::Topic>>(e))
+            emit roomTopicChanged();
+        else if (std::holds_alternative<StateEvent<state::Topic>>(e)) {
+            permissions_.invalidate();
+            emit permissionsChanged();
+        } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
+            emit roomAvatarUrlChanged();
+            emit roomNameChanged();
+            emit roomMemberCountChanged();
+
+            if (roomMemberCount() <= 2) {
+                emit isDirectChanged();
+                emit directChatOtherUserIdChanged();
+            }
+        } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) {
+            this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
+            emit encryptionChanged();
+        }
+    }
 }
 
 void
 TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
 {
-        if (timeline.events.empty())
-                return;
-
-        events.handleSync(timeline);
-
-        using namespace mtx::events;
-
-        for (auto e : timeline.events) {
-                if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
-                        MegolmSessionIndex index;
-                        index.room_id    = room_id_.toStdString();
-                        index.session_id = encryptedEvent->content.session_id;
-                        index.sender_key = encryptedEvent->content.sender_key;
-
-                        auto result = olm::decryptEvent(index, *encryptedEvent);
-                        if (result.event)
-                                e = result.event.value();
-                }
-
-                if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) ||
-                    std::holds_alternative<RoomEvent<msg::CallInvite>>(e) ||
-                    std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) ||
-                    std::holds_alternative<RoomEvent<msg::CallHangUp>>(e))
-                        std::visit(
-                          [this](auto &event) {
-                                  event.room_id = room_id_.toStdString();
-                                  if constexpr (std::is_same_v<std::decay_t<decltype(event)>,
-                                                               RoomEvent<msg::CallAnswer>> ||
-                                                std::is_same_v<std::decay_t<decltype(event)>,
-                                                               RoomEvent<msg::CallHangUp>>)
-                                          emit newCallEvent(event);
-                                  else {
-                                          if (event.sender != http::client()->user_id().to_string())
-                                                  emit newCallEvent(event);
-                                  }
-                          },
-                          e);
-                else if (std::holds_alternative<StateEvent<state::Avatar>>(e))
-                        emit roomAvatarUrlChanged();
-                else if (std::holds_alternative<StateEvent<state::Name>>(e))
-                        emit roomNameChanged();
-                else if (std::holds_alternative<StateEvent<state::Topic>>(e))
-                        emit roomTopicChanged();
-                else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) {
-                        permissions_.invalidate();
-                        emit permissionsChanged();
-                } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
-                        emit roomAvatarUrlChanged();
-                        emit roomNameChanged();
-                        emit roomMemberCountChanged();
-                } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) {
-                        this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
-                        emit encryptionChanged();
-                }
-        }
-        updateLastMessage();
+    if (timeline.events.empty())
+        return;
+
+    events.handleSync(timeline);
+
+    using namespace mtx::events;
+
+    for (auto e : timeline.events) {
+        if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
+            MegolmSessionIndex index(room_id_.toStdString(), encryptedEvent->content);
+
+            auto result = olm::decryptEvent(index, *encryptedEvent);
+            if (result.event)
+                e = result.event.value();
+        }
+
+        if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) ||
+            std::holds_alternative<RoomEvent<msg::CallInvite>>(e) ||
+            std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) ||
+            std::holds_alternative<RoomEvent<msg::CallHangUp>>(e))
+            std::visit(
+              [this](auto &event) {
+                  event.room_id = room_id_.toStdString();
+                  if constexpr (std::is_same_v<std::decay_t<decltype(event)>,
+                                               RoomEvent<msg::CallAnswer>> ||
+                                std::is_same_v<std::decay_t<decltype(event)>,
+                                               RoomEvent<msg::CallHangUp>>)
+                      emit newCallEvent(event);
+                  else {
+                      if (event.sender != http::client()->user_id().to_string())
+                          emit newCallEvent(event);
+                  }
+              },
+              e);
+        else if (std::holds_alternative<StateEvent<state::Avatar>>(e))
+            emit roomAvatarUrlChanged();
+        else if (std::holds_alternative<StateEvent<state::Name>>(e))
+            emit roomNameChanged();
+        else if (std::holds_alternative<StateEvent<state::Topic>>(e))
+            emit roomTopicChanged();
+        else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) {
+            permissions_.invalidate();
+            emit permissionsChanged();
+        } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
+            emit roomAvatarUrlChanged();
+            emit roomNameChanged();
+            emit roomMemberCountChanged();
+        } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) {
+            this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
+            emit encryptionChanged();
+        }
+    }
+    updateLastMessage();
 }
 
 template<typename T>
@@ -894,1124 +876,1191 @@ 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;
+    return true;
 }
 
 template<typename T>
 auto
 isMessage(const mtx::events::Event<T> &)
 {
-        return false;
+    return false;
 }
 
 template<typename T>
 auto
 isMessage(const mtx::events::EncryptedEvent<T> &)
 {
-        return true;
+    return true;
 }
 
 auto
 isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &)
 {
-        return true;
+    return true;
 }
 
 auto
 isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &)
 {
-        return true;
+    return true;
 }
 auto
 isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &)
 {
-        return true;
+    return true;
 }
 
 // Workaround. We also want to see a room at the top, if we just joined it
 auto
 isYourJoin(const mtx::events::StateEvent<mtx::events::state::Member> &e)
 {
-        return e.content.membership == mtx::events::state::Membership::Join &&
-               e.state_key == http::client()->user_id().to_string();
+    return e.content.membership == mtx::events::state::Membership::Join &&
+           e.state_key == http::client()->user_id().to_string();
 }
 template<typename T>
 auto
 isYourJoin(const mtx::events::Event<T> &)
 {
-        return false;
+    return false;
 }
 
 void
 TimelineModel::updateLastMessage()
 {
-        for (auto it = events.size() - 1; it >= 0; --it) {
-                auto event = events.get(it, decryptDescription);
-                if (!event)
-                        continue;
-
-                if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
-                        auto time   = mtx::accessors::origin_server_ts(*event);
-                        uint64_t ts = time.toMSecsSinceEpoch();
-                        auto description =
-                          DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
-                                   QString::fromStdString(http::client()->user_id().to_string()),
-                                   tr("You joined this room."),
-                                   utils::descriptiveTime(time),
-                                   ts,
-                                   time};
-                        if (description != lastMessage_) {
-                                lastMessage_ = description;
-                                emit lastMessageChanged();
-                        }
-                        return;
-                }
-                if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
-                        continue;
-
-                auto description = utils::getMessageDescription(
-                  *event,
-                  QString::fromStdString(http::client()->user_id().to_string()),
-                  cache::displayName(room_id_,
-                                     QString::fromStdString(mtx::accessors::sender(*event))));
-                if (description != lastMessage_) {
-                        lastMessage_ = description;
-                        emit lastMessageChanged();
-                }
-                return;
-        }
+    for (auto it = events.size() - 1; it >= 0; --it) {
+        auto event = events.get(it, decryptDescription);
+        if (!event)
+            continue;
+
+        if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
+            auto time   = mtx::accessors::origin_server_ts(*event);
+            uint64_t ts = time.toMSecsSinceEpoch();
+            auto description =
+              DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
+                       QString::fromStdString(http::client()->user_id().to_string()),
+                       tr("You joined this room."),
+                       utils::descriptiveTime(time),
+                       ts,
+                       time};
+            if (description != lastMessage_) {
+                lastMessage_ = description;
+                emit lastMessageChanged();
+            }
+            return;
+        }
+        if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
+            continue;
+
+        auto description = utils::getMessageDescription(
+          *event,
+          QString::fromStdString(http::client()->user_id().to_string()),
+          cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event))));
+        if (description != lastMessage_) {
+            lastMessage_ = description;
+            emit lastMessageChanged();
+        }
+        return;
+    }
 }
 
 void
 TimelineModel::setCurrentIndex(int index)
 {
-        auto oldIndex = idToIndex(currentId);
-        currentId     = indexToId(index);
-        if (index != oldIndex)
-                emit currentIndexChanged(index);
+    auto oldIndex = idToIndex(currentId);
+    currentId     = indexToId(index);
+    if (index != oldIndex)
+        emit currentIndexChanged(index);
 
-        if (!ChatPage::instance()->isActiveWindow())
-                return;
+    if (!ChatPage::instance()->isActiveWindow())
+        return;
 
-        if (!currentId.startsWith("m")) {
-                auto oldReadIndex =
-                  cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString());
-                auto nextEventIndexAndId =
-                  cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString());
+    if (!currentId.startsWith("m")) {
+        auto oldReadIndex =
+          cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString());
+        auto nextEventIndexAndId =
+          cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString());
 
-                if (nextEventIndexAndId &&
-                    (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
-                        readEvent(nextEventIndexAndId->second);
-                        currentReadId = QString::fromStdString(nextEventIndexAndId->second);
-                }
+        if (nextEventIndexAndId && (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
+            readEvent(nextEventIndexAndId->second);
+            currentReadId = QString::fromStdString(nextEventIndexAndId->second);
         }
+    }
 }
 
 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());
-                }
-        });
+    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());
+        }
+    });
 }
 
 QString
 TimelineModel::displayName(QString id) const
 {
-        return cache::displayName(room_id_, id).toHtmlEscaped();
+    return cache::displayName(room_id_, id).toHtmlEscaped();
 }
 
 QString
 TimelineModel::avatarUrl(QString id) const
 {
-        return cache::avatarUrl(room_id_, id);
+    return cache::avatarUrl(room_id_, id);
 }
 
 QString
 TimelineModel::formatDateSeparator(QDate date) const
 {
-        auto now = QDateTime::currentDateTime();
+    auto now = QDateTime::currentDateTime();
 
-        QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
+    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);
-        }
+    if (now.date().year() == date.year()) {
+        QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
+        fmt = fmt.remove(rx);
+    }
 
-        return date.toString(fmt);
+    return date.toString(fmt);
 }
 
 void
 TimelineModel::viewRawMessage(QString id)
 {
-        auto e = events.get(id.toStdString(), "", false);
-        if (!e)
-                return;
-        std::string ev = mtx::accessors::serialize_event(*e).dump(4);
-        emit showRawMessageDialog(QString::fromStdString(ev));
+    auto e = events.get(id.toStdString(), "", false);
+    if (!e)
+        return;
+    std::string ev = mtx::accessors::serialize_event(*e).dump(4);
+    emit showRawMessageDialog(QString::fromStdString(ev));
 }
 
 void
 TimelineModel::forwardMessage(QString eventId, QString roomId)
 {
-        auto e = events.get(eventId.toStdString(), "");
-        if (!e)
-                return;
+    auto e = events.get(eventId.toStdString(), "");
+    if (!e)
+        return;
 
-        emit forwardToRoom(e, roomId);
+    emit forwardToRoom(e, roomId);
 }
 
 void
 TimelineModel::viewDecryptedRawMessage(QString id)
 {
-        auto e = events.get(id.toStdString(), "");
-        if (!e)
-                return;
+    auto e = events.get(id.toStdString(), "");
+    if (!e)
+        return;
 
-        std::string ev = mtx::accessors::serialize_event(*e).dump(4);
-        emit showRawMessageDialog(QString::fromStdString(ev));
+    std::string ev = mtx::accessors::serialize_event(*e).dump(4);
+    emit showRawMessageDialog(QString::fromStdString(ev));
 }
 
 void
 TimelineModel::openUserProfile(QString userid)
 {
-        UserProfile *userProfile = new UserProfile(room_id_, userid, manager_, this);
-        connect(
-          this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl);
-        emit manager_->openProfile(userProfile);
+    UserProfile *userProfile = new UserProfile(room_id_, userid, manager_, this);
+    connect(this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl);
+    emit manager_->openProfile(userProfile);
 }
 
 void
 TimelineModel::replyAction(QString id)
 {
-        setReply(id);
+    setReply(id);
 }
 
 void
 TimelineModel::editAction(QString id)
 {
-        setEdit(id);
+    setEdit(id);
 }
 
 RelatedInfo
 TimelineModel::relatedInfo(QString id)
 {
-        auto event = events.get(id.toStdString(), "");
-        if (!event)
-                return {};
+    auto event = events.get(id.toStdString(), "");
+    if (!event)
+        return {};
 
-        return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
+    return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
 }
 
 void
 TimelineModel::showReadReceipts(QString id)
 {
-        emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
+    emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
 }
 
 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);
-                  });
+    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;
+    if (id.isEmpty())
+        return -1;
 
-        auto idx = events.idToIndex(id.toStdString());
-        if (idx)
-                return events.size() - *idx - 1;
-        else
-                return -1;
+    auto idx = events.idToIndex(id.toStdString());
+    if (idx)
+        return events.size() - *idx - 1;
+    else
+        return -1;
 }
 
 QString
 TimelineModel::indexToId(int index) const
 {
-        auto id = events.indexToId(events.size() - index - 1);
-        return id ? QString::fromStdString(*id) : "";
+    auto id = events.indexToId(events.size() - index - 1);
+    return id ? QString::fromStdString(*id) : "";
 }
 
 // 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) {
-                        return;
-                }
-                emit dataChanged(index(idx, 0), index(idx, 0));
+    for (const auto &id : event_ids) {
+        read.insert(id);
+        int idx = idToIndex(id);
+        if (idx < 0) {
+            return;
         }
+        emit dataChanged(index(idx, 0), index(idx, 0));
+    }
 }
 
 template<typename T>
 void
 TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType)
 {
-        const auto room_id = room_id_.toStdString();
-
-        using namespace mtx::events;
-        using namespace mtx::identifiers;
-
-        json doc = {{"type", mtx::events::to_string(eventType)},
-                    {"content", json(msg.content)},
-                    {"room_id", room_id}};
-
-        try {
-                mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
-                event.content =
-                  olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
-                event.event_id         = msg.event_id;
-                event.room_id          = room_id;
-                event.sender           = http::client()->user_id().to_string();
-                event.type             = mtx::events::EventType::RoomEncrypted;
-                event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
-
-                emit this->addPendingMessageToStore(event);
-
-                // 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 ChatPage::instance()->showNotification(
-                  tr("Failed to encrypt event, sending aborted!"));
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical(
-                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
-                emit ChatPage::instance()->showNotification(
-                  tr("Failed to encrypt event, sending aborted!"));
-        }
-}
+    const auto room_id = room_id_.toStdString();
 
-struct SendMessageVisitor
-{
-        explicit SendMessageVisitor(TimelineModel *model)
-          : model_(model)
-        {}
-
-        template<typename T, mtx::events::EventType Event>
-        void sendRoomEvent(mtx::events::RoomEvent<T> msg)
-        {
-                if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
-                        auto encInfo = mtx::accessors::file(msg);
-                        if (encInfo)
-                                emit model_->newEncryptedImage(encInfo.value());
-
-                        model_->sendEncryptedMessage(msg, Event);
-                } else {
-                        msg.type = Event;
-                        emit model_->addPendingMessageToStore(msg);
-                }
-        }
+    using namespace mtx::events;
+    using namespace mtx::identifiers;
 
-        // Do-nothing operator for all unhandled events
-        template<typename T>
-        void operator()(const mtx::events::Event<T> &)
-        {}
-
-        // Operator for m.room.message events that contain a msgtype in their content
-        template<typename T,
-                 std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
-        void operator()(mtx::events::RoomEvent<T> msg)
-        {
-                sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg);
-        }
+    json doc = {{"type", mtx::events::to_string(eventType)},
+                {"content", json(msg.content)},
+                {"room_id", room_id}};
 
-        // Special operator for reactions, which are a type of m.room.message, but need to be
-        // handled distinctly for their differences from normal room messages.  Specifically,
-        // reactions need to have the relation outside of ciphertext, or synapse / the homeserver
-        // cannot handle it correctly.  See the MSC for more details:
-        // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption
-        void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg)
-        {
-                msg.type = mtx::events::EventType::Reaction;
-                emit model_->addPendingMessageToStore(msg);
-        }
+    try {
+        mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
+        event.content  = olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
+        event.event_id = msg.event_id;
+        event.room_id  = room_id;
+        event.sender   = http::client()->user_id().to_string();
+        event.type     = mtx::events::EventType::RoomEncrypted;
+        event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
 
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event)
-        {
-                sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>(
-                  event);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event)
-        {
-                sendRoomEvent<mtx::events::msg::CallCandidates,
-                              mtx::events::EventType::CallCandidates>(event);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event)
-        {
-                sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>(
-                  event);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event)
-        {
-                sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>(
-                  event);
-        }
+        emit this->addPendingMessageToStore(event);
 
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
-                              mtx::events::EventType::RoomMessage>(msg);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationReady,
-                              mtx::events::EventType::KeyVerificationReady>(msg);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationStart,
-                              mtx::events::EventType::KeyVerificationStart>(msg);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationAccept,
-                              mtx::events::EventType::KeyVerificationAccept>(msg);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationMac,
-                              mtx::events::EventType::KeyVerificationMac>(msg);
-        }
-
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationKey,
-                              mtx::events::EventType::KeyVerificationKey>(msg);
-        }
+        // 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 ChatPage::instance()->showNotification(
+          tr("Failed to encrypt event, sending aborted!"));
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical(
+          "failed to open outbound megolm session ({}): {}", room_id, e.what());
+        emit ChatPage::instance()->showNotification(
+          tr("Failed to encrypt event, sending aborted!"));
+    }
+}
 
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationDone,
-                              mtx::events::EventType::KeyVerificationDone>(msg);
-        }
+struct SendMessageVisitor
+{
+    explicit SendMessageVisitor(TimelineModel *model)
+      : model_(model)
+    {}
 
-        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg)
-        {
-                sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
-                              mtx::events::EventType::KeyVerificationCancel>(msg);
-        }
-        void operator()(mtx::events::Sticker msg)
-        {
-                msg.type = mtx::events::EventType::Sticker;
-                if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
-                        model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
-                } else
-                        emit model_->addPendingMessageToStore(msg);
-        }
+    template<typename T, mtx::events::EventType Event>
+    void sendRoomEvent(mtx::events::RoomEvent<T> msg)
+    {
+        if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
+            auto encInfo = mtx::accessors::file(msg);
+            if (encInfo)
+                emit model_->newEncryptedImage(encInfo.value());
 
-        TimelineModel *model_;
+            model_->sendEncryptedMessage(msg, Event);
+        } else {
+            msg.type = Event;
+            emit model_->addPendingMessageToStore(msg);
+        }
+    }
+
+    // Do-nothing operator for all unhandled events
+    template<typename T>
+    void operator()(const mtx::events::Event<T> &)
+    {}
+
+    // Operator for m.room.message events that contain a msgtype in their content
+    template<typename T,
+             std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
+    void operator()(mtx::events::RoomEvent<T> msg)
+    {
+        sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg);
+    }
+
+    // Special operator for reactions, which are a type of m.room.message, but need to be
+    // handled distinctly for their differences from normal room messages.  Specifically,
+    // reactions need to have the relation outside of ciphertext, or synapse / the homeserver
+    // cannot handle it correctly.  See the MSC for more details:
+    // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption
+    void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg)
+    {
+        msg.type = mtx::events::EventType::Reaction;
+        emit model_->addPendingMessageToStore(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event)
+    {
+        sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>(event);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event)
+    {
+        sendRoomEvent<mtx::events::msg::CallCandidates, mtx::events::EventType::CallCandidates>(
+          event);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event)
+    {
+        sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>(event);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event)
+    {
+        sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>(event);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
+                      mtx::events::EventType::RoomMessage>(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationReady,
+                      mtx::events::EventType::KeyVerificationReady>(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationStart,
+                      mtx::events::EventType::KeyVerificationStart>(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationAccept,
+                      mtx::events::EventType::KeyVerificationAccept>(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationMac,
+                      mtx::events::EventType::KeyVerificationMac>(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationKey,
+                      mtx::events::EventType::KeyVerificationKey>(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationDone,
+                      mtx::events::EventType::KeyVerificationDone>(msg);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg)
+    {
+        sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
+                      mtx::events::EventType::KeyVerificationCancel>(msg);
+    }
+    void operator()(mtx::events::Sticker msg)
+    {
+        msg.type = mtx::events::EventType::Sticker;
+        if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
+            model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
+        } else
+            emit model_->addPendingMessageToStore(msg);
+    }
+
+    TimelineModel *model_;
 };
 
 void
 TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
 {
-        std::visit(
-          [](auto &msg) {
-                  // gets overwritten for reactions and stickers in SendMessageVisitor
-                  msg.type             = mtx::events::EventType::RoomMessage;
-                  msg.event_id         = "m" + http::client()->generate_txn_id();
-                  msg.sender           = http::client()->user_id().to_string();
-                  msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
-          },
-          event);
+    std::visit(
+      [](auto &msg) {
+          // gets overwritten for reactions and stickers in SendMessageVisitor
+          msg.type             = mtx::events::EventType::RoomMessage;
+          msg.event_id         = "m" + http::client()->generate_txn_id();
+          msg.sender           = http::client()->user_id().to_string();
+          msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
+      },
+      event);
 
-        std::visit(SendMessageVisitor{this}, event);
+    std::visit(SendMessageVisitor{this}, event);
 }
 
 void
 TimelineModel::openMedia(QString eventId)
 {
-        cacheMedia(eventId, [](QString filename) {
-                QDesktopServices::openUrl(QUrl::fromLocalFile(filename));
-        });
+    cacheMedia(eventId,
+               [](QString filename) { QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); });
 }
 
 bool
 TimelineModel::saveMedia(QString eventId) const
 {
-        mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
-        if (!event)
-                return false;
+    mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
+    if (!event)
+        return false;
 
-        QString mxcUrl           = QString::fromStdString(mtx::accessors::url(*event));
-        QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
-        QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(*event));
+    QString mxcUrl           = QString::fromStdString(mtx::accessors::url(*event));
+    QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
+    QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(*event));
 
-        auto encryptionInfo = mtx::accessors::file(*event);
+    auto encryptionInfo = mtx::accessors::file(*event);
 
-        qml_mtx_events::EventType eventType = toRoomEventType(*event);
+    qml_mtx_events::EventType eventType = toRoomEventType(*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 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");
+    }
 
-        const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
-        const QString downloadsFolder =
-          QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
-        const QString openLocation = downloadsFolder + "/" + originalFilename;
-
-        const QString filename = QFileDialog::getSaveFileName(
-          manager_->getWidget(), dialogTitle, openLocation, filterString);
-
-        if (filename.isEmpty())
-                return false;
-
-        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;
-                  }
+    const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
+    const QString downloadsFolder =
+      QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+    const QString openLocation = downloadsFolder + "/" + originalFilename;
 
-                  try {
-                          auto temp = data;
-                          if (encryptionInfo)
-                                  temp = mtx::crypto::to_string(
-                                    mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+    const QString filename =
+      QFileDialog::getSaveFileName(manager_->getWidget(), dialogTitle, openLocation, filterString);
 
-                          QFile file(filename);
+    if (filename.isEmpty())
+        return false;
 
-                          if (!file.open(QIODevice::WriteOnly))
-                                  return;
+    const auto url = mxcUrl.toStdString();
 
-                          file.write(QByteArray(temp.data(), (int)temp.size()));
-                          file.close();
+    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;
+                                 }
 
-                          return;
-                  } catch (const std::exception &e) {
-                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
-                  }
-          });
-        return true;
+                                 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();
+
+                                     return;
+                                 } catch (const std::exception &e) {
+                                     nhlog::ui()->warn("Error while saving file to: {}", e.what());
+                                 }
+                             });
+    return true;
 }
 
 void
 TimelineModel::cacheMedia(QString eventId, std::function<void(const QString)> callback)
 {
-        mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
-        if (!event)
-                return;
+    mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
+    if (!event)
+        return;
 
-        QString mxcUrl           = QString::fromStdString(mtx::accessors::url(*event));
-        QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
-        QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(*event));
+    QString mxcUrl           = QString::fromStdString(mtx::accessors::url(*event));
+    QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
+    QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(*event));
 
-        auto encryptionInfo = mtx::accessors::file(*event);
+    auto encryptionInfo = mtx::accessors::file(*event);
 
-        // If the message is a link to a non mxcUrl, don't download it
-        if (!mxcUrl.startsWith("mxc://")) {
-                emit mediaCached(mxcUrl, mxcUrl);
-                return;
-        }
+    // 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();
-        const auto name = QString(mxcUrl).remove("mxc://");
-        QFileInfo filename(QString("%1/media_cache/%2.%3")
-                             .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
-                             .arg(name)
-                             .arg(suffix));
-        if (QDir::cleanPath(name) != name) {
-                nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
-                return;
-        }
+    QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
 
-        QDir().mkpath(filename.path());
+    const auto url  = mxcUrl.toStdString();
+    const auto name = QString(mxcUrl).remove("mxc://");
+    QFileInfo filename(QString("%1/media_cache/%2.%3")
+                         .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                         .arg(name)
+                         .arg(suffix));
+    if (QDir::cleanPath(name) != name) {
+        nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
+        return;
+    }
 
-        if (filename.isReadable()) {
+    QDir().mkpath(filename.path());
+
+    if (filename.isReadable()) {
 #if defined(Q_OS_WIN)
-                emit mediaCached(mxcUrl, filename.filePath());
+        emit mediaCached(mxcUrl, filename.filePath());
 #else
-                emit mediaCached(mxcUrl, "file://" + filename.filePath());
+        emit mediaCached(mxcUrl, "file://" + filename.filePath());
 #endif
-                if (callback) {
-                        callback(filename.filePath());
-                }
-                return;
-        }
-
-        http::client()->download(
-          url,
-          [this, callback, 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(), (int)temp.size()));
-                          file.close();
-
-                          if (callback) {
-                                  callback(filename.filePath());
-                          }
-                  } catch (const std::exception &e) {
-                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
-                  }
+        if (callback) {
+            callback(filename.filePath());
+        }
+        return;
+    }
+
+    http::client()->download(
+      url,
+      [this, callback, 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(), (int)temp.size()));
+              file.close();
+
+              if (callback) {
+                  callback(filename.filePath());
+              }
+          } catch (const std::exception &e) {
+              nhlog::ui()->warn("Error while saving file to: {}", e.what());
+          }
 
 #if defined(Q_OS_WIN)
-                  emit mediaCached(mxcUrl, filename.filePath());
+          emit mediaCached(mxcUrl, filename.filePath());
 #else
-                  emit mediaCached(mxcUrl, "file://" + filename.filePath());
+          emit mediaCached(mxcUrl, "file://" + filename.filePath());
 #endif
-          });
+      });
 }
 
 void
 TimelineModel::cacheMedia(QString eventId)
 {
-        cacheMedia(eventId, NULL);
+    cacheMedia(eventId, NULL);
 }
 
 void
 TimelineModel::showEvent(QString eventId)
 {
-        using namespace std::chrono_literals;
-        if (idToIndex(eventId) != -1) {
-                eventIdToShow = eventId;
-                emit scrollTargetChanged();
-                showEventTimer.start(50ms);
+    using namespace std::chrono_literals;
+    // Direct to eventId
+    if (eventId[0] == '$') {
+        int idx = idToIndex(eventId);
+        if (idx == -1) {
+            nhlog::ui()->warn("Scrolling to event id {}, failed - no known index",
+                              eventId.toStdString());
+            return;
         }
+        eventIdToShow = eventId;
+        emit scrollTargetChanged();
+        showEventTimer.start(50ms);
+        return;
+    }
+    // to message index
+    eventId       = indexToId(eventId.toInt());
+    eventIdToShow = eventId;
+    emit scrollTargetChanged();
+    showEventTimer.start(50ms);
+    return;
 }
 
 void
 TimelineModel::eventShown()
 {
-        eventIdToShow.clear();
-        emit scrollTargetChanged();
+    eventIdToShow.clear();
+    emit scrollTargetChanged();
 }
 
 QString
 TimelineModel::scrollTarget() const
 {
-        return eventIdToShow;
+    return eventIdToShow;
 }
 
 void
 TimelineModel::scrollTimerEvent()
 {
-        if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) {
-                showEventTimer.stop();
-                showEventTimerCounter = 0;
-        } else {
-                emit scrollToIndex(idToIndex(eventIdToShow));
-                showEventTimerCounter++;
-        }
+    if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) {
+        showEventTimer.stop();
+        showEventTimerCounter = 0;
+    } else {
+        emit scrollToIndex(idToIndex(eventIdToShow));
+        showEventTimerCounter++;
+    }
 }
 
 void
 TimelineModel::requestKeyForEvent(QString id)
 {
-        auto encrypted_event = events.get(id.toStdString(), "", false);
-        if (encrypted_event) {
-                if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                      encrypted_event))
-                        events.requestSession(*ev, true);
-        }
+    auto encrypted_event = events.get(id.toStdString(), "", false);
+    if (encrypted_event) {
+        if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+              encrypted_event))
+            events.requestSession(*ev, true);
+    }
 }
 
 void
 TimelineModel::copyLinkToEvent(QString eventId) const
 {
-        QStringList vias;
+    QStringList vias;
 
-        auto alias = cache::client()->getRoomAliases(room_id_.toStdString());
-        QString room;
-        if (alias) {
-                room = QString::fromStdString(alias->alias);
-                if (room.isEmpty() && !alias->alt_aliases.empty()) {
-                        room = QString::fromStdString(alias->alt_aliases.front());
-                }
+    auto alias = cache::client()->getRoomAliases(room_id_.toStdString());
+    QString room;
+    if (alias) {
+        room = QString::fromStdString(alias->alias);
+        if (room.isEmpty() && !alias->alt_aliases.empty()) {
+            room = QString::fromStdString(alias->alt_aliases.front());
         }
+    }
 
-        if (room.isEmpty())
-                room = room_id_;
+    if (room.isEmpty())
+        room = room_id_;
 
-        vias.push_back(QString("via=%1").arg(QString(
-          QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname())))));
-        auto members = cache::getMembers(room_id_.toStdString(), 0, 100);
-        for (const auto &m : members) {
-                if (vias.size() >= 4)
-                        break;
+    vias.push_back(QString("via=%1").arg(QString(
+      QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname())))));
+    auto members = cache::getMembers(room_id_.toStdString(), 0, 100);
+    for (const auto &m : members) {
+        if (vias.size() >= 4)
+            break;
 
-                auto user_id =
-                  mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString());
-                QString server = QString("via=%1").arg(
-                  QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname()))));
+        auto user_id   = mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString());
+        QString server = QString("via=%1").arg(
+          QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname()))));
 
-                if (!vias.contains(server))
-                        vias.push_back(server);
-        }
+        if (!vias.contains(server))
+            vias.push_back(server);
+    }
 
-        auto link = QString("https://matrix.to/#/%1/%2?%3")
-                      .arg(QString(QUrl::toPercentEncoding(room)),
-                           QString(QUrl::toPercentEncoding(eventId)),
-                           vias.join('&'));
+    auto link = QString("https://matrix.to/#/%1/%2?%3")
+                  .arg(QString(QUrl::toPercentEncoding(room)),
+                       QString(QUrl::toPercentEncoding(eventId)),
+                       vias.join('&'));
 
-        QGuiApplication::clipboard()->setText(link);
+    QGuiApplication::clipboard()->setText(link);
 }
 
 QString
 TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg)
 {
-        QString temp =
-          tr("%1 and %2 are typing.",
-             "Multiple users are typing. First argument is a comma separated list of potentially "
-             "multiple users. Second argument is the last user of that list. (If only one user is "
-             "typing, %1 is empty. You should still use it in your string though to silence Qt "
-             "warnings.)",
-             (int)users.size());
+    QString temp =
+      tr("%1 and %2 are typing.",
+         "Multiple users are typing. First argument is a comma separated list of potentially "
+         "multiple users. Second argument is the last user of that list. (If only one user is "
+         "typing, %1 is empty. You should still use it in your string though to silence Qt "
+         "warnings.)",
+         (int)users.size());
 
-        if (users.empty()) {
-                return "";
-        }
+    if (users.empty()) {
+        return "";
+    }
 
-        QStringList uidWithoutLast;
+    QStringList uidWithoutLast;
 
-        auto formatUser = [this, bg](const QString &user_id) -> QString {
-                auto uncoloredUsername = utils::replaceEmoji(displayName(user_id));
-                QString prefix =
-                  QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name());
+    auto formatUser = [this, bg](const QString &user_id) -> QString {
+        auto uncoloredUsername = utils::replaceEmoji(displayName(user_id));
+        QString prefix =
+          QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name());
 
-                // color only parts that don't have a font already specified
-                QString coloredUsername;
-                int index = 0;
-                do {
-                        auto startIndex = uncoloredUsername.indexOf("<font", index);
+        // color only parts that don't have a font already specified
+        QString coloredUsername;
+        int index = 0;
+        do {
+            auto startIndex = uncoloredUsername.indexOf("<font", index);
 
-                        if (startIndex - index != 0)
-                                coloredUsername +=
-                                  prefix +
-                                  uncoloredUsername.midRef(
-                                    index, startIndex > 0 ? startIndex - index : -1) +
-                                  "</font>";
+            if (startIndex - index != 0)
+                coloredUsername +=
+                  prefix +
+                  uncoloredUsername.midRef(index, startIndex > 0 ? startIndex - index : -1) +
+                  "</font>";
 
-                        auto endIndex = uncoloredUsername.indexOf("</font>", startIndex);
-                        if (endIndex > 0)
-                                endIndex += sizeof("</font>") - 1;
+            auto endIndex = uncoloredUsername.indexOf("</font>", startIndex);
+            if (endIndex > 0)
+                endIndex += sizeof("</font>") - 1;
 
-                        if (endIndex - startIndex != 0)
-                                coloredUsername +=
-                                  uncoloredUsername.midRef(startIndex, endIndex - startIndex);
+            if (endIndex - startIndex != 0)
+                coloredUsername += uncoloredUsername.midRef(startIndex, endIndex - startIndex);
 
-                        index = endIndex;
-                } while (index > 0 && index < uncoloredUsername.size());
+            index = endIndex;
+        } while (index > 0 && index < uncoloredUsername.size());
 
-                return coloredUsername;
-        };
+        return coloredUsername;
+    };
 
-        for (size_t i = 0; i + 1 < users.size(); i++) {
-                uidWithoutLast.append(formatUser(users[i]));
-        }
+    for (size_t i = 0; i + 1 < users.size(); i++) {
+        uidWithoutLast.append(formatUser(users[i]));
+    }
 
-        return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back()));
+    return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back()));
 }
 
 QString
 TimelineModel::formatJoinRuleEvent(QString id)
 {
-        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
-        if (!e)
-                return "";
-
-        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e);
-        if (!event)
-                return "";
-
-        QString user = QString::fromStdString(event->sender);
-        QString name = utils::replaceEmoji(displayName(user));
-
-        switch (event->content.join_rule) {
-        case mtx::events::state::JoinRule::Public:
-                return tr("%1 opened the room to the public.").arg(name);
-        case mtx::events::state::JoinRule::Invite:
-                return tr("%1 made this room require and invitation to join.").arg(name);
-        default:
-                // Currently, knock and private are reserved keywords and not implemented in Matrix.
-                return "";
-        }
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return "";
+
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e);
+    if (!event)
+        return "";
+
+    QString user = QString::fromStdString(event->sender);
+    QString name = utils::replaceEmoji(displayName(user));
+
+    switch (event->content.join_rule) {
+    case mtx::events::state::JoinRule::Public:
+        return tr("%1 opened the room to the public.").arg(name);
+    case mtx::events::state::JoinRule::Invite:
+        return tr("%1 made this room require and invitation to join.").arg(name);
+    case mtx::events::state::JoinRule::Knock:
+        return tr("%1 allowed to join this room by knocking.").arg(name);
+    case mtx::events::state::JoinRule::Restricted: {
+        QStringList rooms;
+        for (const auto &r : event->content.allow) {
+            if (r.type == mtx::events::state::JoinAllowanceType::RoomMembership)
+                rooms.push_back(QString::fromStdString(r.room_id));
+        }
+        return tr("%1 allowed members of the following rooms to automatically join this "
+                  "room: %2")
+          .arg(name)
+          .arg(rooms.join(", "));
+    }
+    default:
+        // Currently, knock and private are reserved keywords and not implemented in Matrix.
+        return "";
+    }
 }
 
 QString
 TimelineModel::formatGuestAccessEvent(QString id)
 {
-        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
-        if (!e)
-                return "";
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return "";
 
-        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e);
-        if (!event)
-                return "";
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e);
+    if (!event)
+        return "";
 
-        QString user = QString::fromStdString(event->sender);
-        QString name = utils::replaceEmoji(displayName(user));
+    QString user = QString::fromStdString(event->sender);
+    QString name = utils::replaceEmoji(displayName(user));
 
-        switch (event->content.guest_access) {
-        case mtx::events::state::AccessState::CanJoin:
-                return tr("%1 made the room open to guests.").arg(name);
-        case mtx::events::state::AccessState::Forbidden:
-                return tr("%1 has closed the room to guest access.").arg(name);
-        default:
-                return "";
-        }
+    switch (event->content.guest_access) {
+    case mtx::events::state::AccessState::CanJoin:
+        return tr("%1 made the room open to guests.").arg(name);
+    case mtx::events::state::AccessState::Forbidden:
+        return tr("%1 has closed the room to guest access.").arg(name);
+    default:
+        return "";
+    }
 }
 
 QString
 TimelineModel::formatHistoryVisibilityEvent(QString id)
 {
-        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
-        if (!e)
-                return "";
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return "";
 
-        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e);
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e);
 
-        if (!event)
-                return "";
-
-        QString user = QString::fromStdString(event->sender);
-        QString name = utils::replaceEmoji(displayName(user));
-
-        switch (event->content.history_visibility) {
-        case mtx::events::state::Visibility::WorldReadable:
-                return tr("%1 made the room history world readable. Events may be now read by "
-                          "non-joined people.")
-                  .arg(name);
-        case mtx::events::state::Visibility::Shared:
-                return tr("%1 set the room history visible to members from this point on.")
-                  .arg(name);
-        case mtx::events::state::Visibility::Invited:
-                return tr("%1 set the room history visible to members since they were invited.")
-                  .arg(name);
-        case mtx::events::state::Visibility::Joined:
-                return tr("%1 set the room history visible to members since they joined the room.")
-                  .arg(name);
-        default:
-                return "";
-        }
+    if (!event)
+        return "";
+
+    QString user = QString::fromStdString(event->sender);
+    QString name = utils::replaceEmoji(displayName(user));
+
+    switch (event->content.history_visibility) {
+    case mtx::events::state::Visibility::WorldReadable:
+        return tr("%1 made the room history world readable. Events may be now read by "
+                  "non-joined people.")
+          .arg(name);
+    case mtx::events::state::Visibility::Shared:
+        return tr("%1 set the room history visible to members from this point on.").arg(name);
+    case mtx::events::state::Visibility::Invited:
+        return tr("%1 set the room history visible to members since they were invited.").arg(name);
+    case mtx::events::state::Visibility::Joined:
+        return tr("%1 set the room history visible to members since they joined the room.")
+          .arg(name);
+    default:
+        return "";
+    }
 }
 
 QString
 TimelineModel::formatPowerLevelEvent(QString id)
 {
-        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
-        if (!e)
-                return "";
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return "";
 
-        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e);
-        if (!event)
-                return "";
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e);
+    if (!event)
+        return "";
 
-        QString user = QString::fromStdString(event->sender);
-        QString name = utils::replaceEmoji(displayName(user));
+    QString user = QString::fromStdString(event->sender);
+    QString name = utils::replaceEmoji(displayName(user));
 
-        // TODO: power levels rendering is actually a bit complex. work on this later.
-        return tr("%1 has changed the room's permissions.").arg(name);
+    // TODO: power levels rendering is actually a bit complex. work on this later.
+    return tr("%1 has changed the room's permissions.").arg(name);
 }
 
-QString
-TimelineModel::formatMemberEvent(QString id)
+void
+TimelineModel::acceptKnock(QString id)
 {
-        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
-        if (!e)
-                return "";
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return;
 
-        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
-        if (!event)
-                return "";
-
-        mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr;
-        if (!event->unsigned_data.replaces_state.empty()) {
-                auto tempPrevEvent =
-                  events.get(event->unsigned_data.replaces_state, event->event_id);
-                if (tempPrevEvent) {
-                        prevEvent =
-                          std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(
-                            tempPrevEvent);
-                }
-        }
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
+    if (!event)
+        return;
 
-        QString user = QString::fromStdString(event->state_key);
-        QString name = utils::replaceEmoji(displayName(user));
-        QString rendered;
-
-        // see table https://matrix.org/docs/spec/client_server/latest#m-room-member
-        using namespace mtx::events::state;
-        switch (event->content.membership) {
-        case Membership::Invite:
-                rendered = tr("%1 was invited.").arg(name);
-                break;
-        case Membership::Join:
-                if (prevEvent && prevEvent->content.membership == Membership::Join) {
-                        QString oldName = QString::fromStdString(prevEvent->content.display_name);
-
-                        bool displayNameChanged =
-                          prevEvent->content.display_name != event->content.display_name;
-                        bool avatarChanged =
-                          prevEvent->content.avatar_url != event->content.avatar_url;
-
-                        if (displayNameChanged && avatarChanged)
-                                rendered = tr("%1 has changed their avatar and changed their "
-                                              "display name to %2.")
-                                             .arg(oldName, name);
-                        else if (displayNameChanged)
-                                rendered =
-                                  tr("%1 has changed their display name to %2.").arg(oldName, name);
-                        else if (avatarChanged)
-                                rendered = tr("%1 changed their avatar.").arg(name);
-                        else
-                                rendered = tr("%1 changed some profile info.").arg(name);
-                        // the case of nothing changed but join follows join shouldn't happen, so
-                        // just show it as join
-                } else {
-                        rendered = tr("%1 joined.").arg(name);
-                }
-                break;
-        case Membership::Leave:
-                if (!prevEvent) // Should only ever happen temporarily
-                        return "";
-
-                if (prevEvent->content.membership == Membership::Invite) {
-                        if (event->state_key == event->sender)
-                                rendered = tr("%1 rejected their invite.").arg(name);
-                        else
-                                rendered = tr("Revoked the invite to %1.").arg(name);
-                } else if (prevEvent->content.membership == Membership::Join) {
-                        if (event->state_key == event->sender)
-                                rendered = tr("%1 left the room.").arg(name);
-                        else
-                                rendered = tr("Kicked %1.").arg(name);
-                } else if (prevEvent->content.membership == Membership::Ban) {
-                        rendered = tr("Unbanned %1.").arg(name);
-                } else if (prevEvent->content.membership == Membership::Knock) {
-                        if (event->state_key == event->sender)
-                                rendered = tr("%1 redacted their knock.").arg(name);
-                        else
-                                rendered = tr("Rejected the knock from %1.").arg(name);
-                } else
-                        return tr("%1 left after having already left!",
-                                  "This is a leave event after the user already left and shouldn't "
-                                  "happen apart from state resets")
-                          .arg(name);
-                break;
-
-        case Membership::Ban:
-                rendered = tr("%1 was banned.").arg(name);
-                break;
-        case Membership::Knock:
-                rendered = tr("%1 knocked.").arg(name);
-                break;
-        }
+    if (!permissions_.canInvite())
+        return;
 
-        if (event->content.reason != "") {
-                rendered +=
-                  " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason));
-        }
+    if (cache::isRoomMember(event->state_key, room_id_.toStdString()))
+        return;
 
-        return rendered;
+    using namespace mtx::events::state;
+    if (event->content.membership != Membership::Knock)
+        return;
+
+    ChatPage::instance()->inviteUser(QString::fromStdString(event->state_key), "");
+}
+
+bool
+TimelineModel::showAcceptKnockButton(QString id)
+{
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return false;
+
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
+    if (!event)
+        return false;
+
+    if (!permissions_.canInvite())
+        return false;
+
+    if (cache::isRoomMember(event->state_key, room_id_.toStdString()))
+        return false;
+
+    using namespace mtx::events::state;
+    return event->content.membership == Membership::Knock;
+}
+
+QString
+TimelineModel::formatMemberEvent(QString id)
+{
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return "";
+
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
+    if (!event)
+        return "";
+
+    mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr;
+    if (!event->unsigned_data.replaces_state.empty()) {
+        auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id);
+        if (tempPrevEvent) {
+            prevEvent =
+              std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent);
+        }
+    }
+
+    QString user = QString::fromStdString(event->state_key);
+    QString name = utils::replaceEmoji(displayName(user));
+    QString rendered;
+
+    // see table https://matrix.org/docs/spec/client_server/latest#m-room-member
+    using namespace mtx::events::state;
+    switch (event->content.membership) {
+    case Membership::Invite:
+        rendered = tr("%1 was invited.").arg(name);
+        break;
+    case Membership::Join:
+        if (prevEvent && prevEvent->content.membership == Membership::Join) {
+            QString oldName = utils::replaceEmoji(
+              QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped());
+
+            bool displayNameChanged =
+              prevEvent->content.display_name != event->content.display_name;
+            bool avatarChanged = prevEvent->content.avatar_url != event->content.avatar_url;
+
+            if (displayNameChanged && avatarChanged)
+                rendered = tr("%1 has changed their avatar and changed their "
+                              "display name to %2.")
+                             .arg(oldName, name);
+            else if (displayNameChanged)
+                rendered = tr("%1 has changed their display name to %2.").arg(oldName, name);
+            else if (avatarChanged)
+                rendered = tr("%1 changed their avatar.").arg(name);
+            else
+                rendered = tr("%1 changed some profile info.").arg(name);
+            // the case of nothing changed but join follows join shouldn't happen, so
+            // just show it as join
+        } else {
+            if (event->content.join_authorised_via_users_server.empty())
+                rendered = tr("%1 joined.").arg(name);
+            else
+                rendered =
+                  tr("%1 joined via authorisation from %2's server.")
+                    .arg(name)
+                    .arg(QString::fromStdString(event->content.join_authorised_via_users_server));
+        }
+        break;
+    case Membership::Leave:
+        if (!prevEvent) // Should only ever happen temporarily
+            return "";
+
+        if (prevEvent->content.membership == Membership::Invite) {
+            if (event->state_key == event->sender)
+                rendered = tr("%1 rejected their invite.").arg(name);
+            else
+                rendered = tr("Revoked the invite to %1.").arg(name);
+        } else if (prevEvent->content.membership == Membership::Join) {
+            if (event->state_key == event->sender)
+                rendered = tr("%1 left the room.").arg(name);
+            else
+                rendered = tr("Kicked %1.").arg(name);
+        } else if (prevEvent->content.membership == Membership::Ban) {
+            rendered = tr("Unbanned %1.").arg(name);
+        } else if (prevEvent->content.membership == Membership::Knock) {
+            if (event->state_key == event->sender)
+                rendered = tr("%1 redacted their knock.").arg(name);
+            else
+                rendered = tr("Rejected the knock from %1.").arg(name);
+        } else
+            return tr("%1 left after having already left!",
+                      "This is a leave event after the user already left and shouldn't "
+                      "happen apart from state resets")
+              .arg(name);
+        break;
+
+    case Membership::Ban:
+        rendered = tr("%1 was banned.").arg(name);
+        break;
+    case Membership::Knock:
+        rendered = tr("%1 knocked.").arg(name);
+        break;
+    }
+
+    if (event->content.reason != "") {
+        rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason));
+    }
+
+    return rendered;
 }
 
 void
 TimelineModel::setEdit(QString newEdit)
 {
-        if (newEdit.isEmpty()) {
-                resetEdit();
-                return;
-        }
+    if (newEdit.isEmpty()) {
+        resetEdit();
+        return;
+    }
+
+    if (edit_.isEmpty()) {
+        this->textBeforeEdit  = input()->text();
+        this->replyBeforeEdit = reply_;
+        nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString());
+    }
+
+    if (edit_ != newEdit) {
+        auto ev = events.get(newEdit.toStdString(), "");
+        if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
+            auto e = *ev;
+            setReply(QString::fromStdString(mtx::accessors::relations(e).reply_to().value_or("")));
+
+            auto msgType = mtx::accessors::msg_type(e);
+            if (msgType == mtx::events::MessageType::Text ||
+                msgType == mtx::events::MessageType::Notice ||
+                msgType == mtx::events::MessageType::Emote) {
+                auto relInfo  = relatedInfo(newEdit);
+                auto editText = relInfo.quoted_body;
+
+                if (!relInfo.quoted_formatted_body.isEmpty()) {
+                    auto matches =
+                      conf::strings::matrixToLink.globalMatch(relInfo.quoted_formatted_body);
+                    std::map<QString, QString> reverseNameMapping;
+                    while (matches.hasNext()) {
+                        auto m                            = matches.next();
+                        reverseNameMapping[m.captured(2)] = m.captured(1);
+                    }
+
+                    for (const auto &[user, link] : reverseNameMapping) {
+                        // TODO(Nico): html unescape the user name
+                        editText.replace(user, QStringLiteral("[%1](%2)").arg(user, link));
+                    }
+                }
 
-        if (edit_.isEmpty()) {
-                this->textBeforeEdit  = input()->text();
-                this->replyBeforeEdit = reply_;
-                nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString());
-        }
+                if (msgType == mtx::events::MessageType::Emote)
+                    input()->setText("/me " + editText);
+                else
+                    input()->setText(editText);
+            } else {
+                input()->setText("");
+            }
 
-        if (edit_ != newEdit) {
-                auto ev = events.get(newEdit.toStdString(), "");
-                if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
-                        auto e = *ev;
-                        setReply(QString::fromStdString(
-                          mtx::accessors::relations(e).reply_to().value_or("")));
-
-                        auto msgType = mtx::accessors::msg_type(e);
-                        if (msgType == mtx::events::MessageType::Text ||
-                            msgType == mtx::events::MessageType::Notice ||
-                            msgType == mtx::events::MessageType::Emote) {
-                                auto relInfo  = relatedInfo(newEdit);
-                                auto editText = relInfo.quoted_body;
-
-                                if (!relInfo.quoted_formatted_body.isEmpty()) {
-                                        auto matches = conf::strings::matrixToLink.globalMatch(
-                                          relInfo.quoted_formatted_body);
-                                        std::map<QString, QString> reverseNameMapping;
-                                        while (matches.hasNext()) {
-                                                auto m                            = matches.next();
-                                                reverseNameMapping[m.captured(2)] = m.captured(1);
-                                        }
-
-                                        for (const auto &[user, link] : reverseNameMapping) {
-                                                // TODO(Nico): html unescape the user name
-                                                editText.replace(
-                                                  user, QStringLiteral("[%1](%2)").arg(user, link));
-                                        }
-                                }
-
-                                if (msgType == mtx::events::MessageType::Emote)
-                                        input()->setText("/me " + editText);
-                                else
-                                        input()->setText(editText);
-                        } else {
-                                input()->setText("");
-                        }
-
-                        edit_ = newEdit;
-                } else {
-                        resetReply();
-
-                        input()->setText("");
-                        edit_ = "";
-                }
-                emit editChanged(edit_);
+            edit_ = newEdit;
+        } else {
+            resetReply();
+
+            input()->setText("");
+            edit_ = "";
         }
+        emit editChanged(edit_);
+    }
 }
 
 void
 TimelineModel::resetEdit()
 {
-        if (!edit_.isEmpty()) {
-                edit_ = "";
-                emit editChanged(edit_);
-                nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString());
-                input()->setText(textBeforeEdit);
-                textBeforeEdit.clear();
-                if (replyBeforeEdit.isEmpty())
-                        resetReply();
-                else
-                        setReply(replyBeforeEdit);
-                replyBeforeEdit.clear();
-        }
+    if (!edit_.isEmpty()) {
+        edit_ = "";
+        emit editChanged(edit_);
+        nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString());
+        input()->setText(textBeforeEdit);
+        textBeforeEdit.clear();
+        if (replyBeforeEdit.isEmpty())
+            resetReply();
+        else
+            setReply(replyBeforeEdit);
+        replyBeforeEdit.clear();
+    }
 }
 
 QString
 TimelineModel::roomName() const
 {
-        auto info = cache::getRoomInfo({room_id_.toStdString()});
+    auto info = cache::getRoomInfo({room_id_.toStdString()});
 
-        if (!info.count(room_id_))
-                return "";
-        else
-                return utils::replaceEmoji(
-                  QString::fromStdString(info[room_id_].name).toHtmlEscaped());
+    if (!info.count(room_id_))
+        return "";
+    else
+        return utils::replaceEmoji(QString::fromStdString(info[room_id_].name).toHtmlEscaped());
 }
 
 QString
 TimelineModel::plainRoomName() const
 {
-        auto info = cache::getRoomInfo({room_id_.toStdString()});
+    auto info = cache::getRoomInfo({room_id_.toStdString()});
 
-        if (!info.count(room_id_))
-                return "";
-        else
-                return QString::fromStdString(info[room_id_].name);
+    if (!info.count(room_id_))
+        return "";
+    else
+        return QString::fromStdString(info[room_id_].name);
 }
 
 QString
 TimelineModel::roomAvatarUrl() const
 {
-        auto info = cache::getRoomInfo({room_id_.toStdString()});
+    auto info = cache::getRoomInfo({room_id_.toStdString()});
 
-        if (!info.count(room_id_))
-                return "";
-        else
-                return QString::fromStdString(info[room_id_].avatar_url);
+    if (!info.count(room_id_))
+        return "";
+    else
+        return QString::fromStdString(info[room_id_].avatar_url);
 }
 
 QString
 TimelineModel::roomTopic() const
 {
-        auto info = cache::getRoomInfo({room_id_.toStdString()});
+    auto info = cache::getRoomInfo({room_id_.toStdString()});
 
-        if (!info.count(room_id_))
-                return "";
-        else
-                return utils::replaceEmoji(utils::linkifyMessage(
-                  QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
+    if (!info.count(room_id_))
+        return "";
+    else
+        return utils::replaceEmoji(
+          utils::linkifyMessage(QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
 }
 
 crypto::Trust
 TimelineModel::trustlevel() const
 {
-        if (!isEncrypted_)
-                return crypto::Trust::Unverified;
+    if (!isEncrypted_)
+        return crypto::Trust::Unverified;
 
-        return cache::client()->roomVerificationStatus(room_id_.toStdString());
+    return cache::client()->roomVerificationStatus(room_id_.toStdString());
 }
 
 int
 TimelineModel::roomMemberCount() const
 {
-        return (int)cache::client()->memberCount(room_id_.toStdString());
+    return (int)cache::client()->memberCount(room_id_.toStdString());
+}
+
+QString
+TimelineModel::directChatOtherUserId() const
+{
+    if (roomMemberCount() < 3) {
+        QString id;
+        for (auto member : cache::getMembers(room_id_.toStdString()))
+            if (member.user_id != UserSettings::instance()->userId())
+                id = member.user_id;
+        return id;
+    } else
+        return "";
 }
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index aa07fe01f15d452ec1a181c36a480ec11abdc893..f16529e21790157e67a9070be24c85f61dc05f14 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -39,95 +39,95 @@ Q_NAMESPACE
 
 enum EventType
 {
-        // Unsupported event
-        Unsupported,
-        /// m.room_key_request
-        KeyRequest,
-        /// m.reaction,
-        Reaction,
-        /// m.room.aliases
-        Aliases,
-        /// m.room.avatar
-        Avatar,
-        /// m.call.invite
-        CallInvite,
-        /// m.call.answer
-        CallAnswer,
-        /// m.call.hangup
-        CallHangUp,
-        /// m.call.candidates
-        CallCandidates,
-        /// m.room.canonical_alias
-        CanonicalAlias,
-        /// m.room.create
-        RoomCreate,
-        /// m.room.encrypted.
-        Encrypted,
-        /// m.room.encryption.
-        Encryption,
-        /// m.room.guest_access
-        RoomGuestAccess,
-        /// m.room.history_visibility
-        RoomHistoryVisibility,
-        /// m.room.join_rules
-        RoomJoinRules,
-        /// 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,
-        KeyVerificationRequest,
-        KeyVerificationStart,
-        KeyVerificationMac,
-        KeyVerificationAccept,
-        KeyVerificationCancel,
-        KeyVerificationKey,
-        KeyVerificationDone,
-        KeyVerificationReady,
-        //! m.image_pack, currently im.ponies.room_emotes
-        ImagePackInRoom,
-        //! m.image_pack, currently im.ponies.user_emotes
-        ImagePackInAccountData,
-        //! m.image_pack.rooms, currently im.ponies.emote_rooms
-        ImagePackRooms,
+    // Unsupported event
+    Unsupported,
+    /// m.room_key_request
+    KeyRequest,
+    /// m.reaction,
+    Reaction,
+    /// m.room.aliases
+    Aliases,
+    /// m.room.avatar
+    Avatar,
+    /// m.call.invite
+    CallInvite,
+    /// m.call.answer
+    CallAnswer,
+    /// m.call.hangup
+    CallHangUp,
+    /// m.call.candidates
+    CallCandidates,
+    /// m.room.canonical_alias
+    CanonicalAlias,
+    /// m.room.create
+    RoomCreate,
+    /// m.room.encrypted.
+    Encrypted,
+    /// m.room.encryption.
+    Encryption,
+    /// m.room.guest_access
+    RoomGuestAccess,
+    /// m.room.history_visibility
+    RoomHistoryVisibility,
+    /// m.room.join_rules
+    RoomJoinRules,
+    /// 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,
+    KeyVerificationRequest,
+    KeyVerificationStart,
+    KeyVerificationMac,
+    KeyVerificationAccept,
+    KeyVerificationCancel,
+    KeyVerificationKey,
+    KeyVerificationDone,
+    KeyVerificationReady,
+    //! m.image_pack, currently im.ponies.room_emotes
+    ImagePackInRoom,
+    //! m.image_pack, currently im.ponies.user_emotes
+    ImagePackInAccountData,
+    //! m.image_pack.rooms, currently im.ponies.emote_rooms
+    ImagePackRooms,
 };
 Q_ENUM_NS(EventType)
 mtx::events::EventType fromRoomEventType(qml_mtx_events::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,
+    //! 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,
 };
 Q_ENUM_NS(EventState)
 }
@@ -135,312 +135,329 @@ Q_ENUM_NS(EventState)
 class StateKeeper
 {
 public:
-        StateKeeper(std::function<void()> &&fn)
-          : fn_(std::move(fn))
-        {}
+    StateKeeper(std::function<void()> &&fn)
+      : fn_(std::move(fn))
+    {}
 
-        ~StateKeeper() { fn_(); }
+    ~StateKeeper() { fn_(); }
 
 private:
-        std::function<void()> fn_;
+    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;
+    //! 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)
-        Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY
-                     typingUsersChanged)
-        Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged)
-        Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply)
-        Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit)
-        Q_PROPERTY(
-          bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
-        Q_PROPERTY(QString roomId READ roomId CONSTANT)
-        Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
-        Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged)
-        Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
-        Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
-        Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
-        Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged)
-        Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
-        Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged)
-        Q_PROPERTY(InputBar *input READ input CONSTANT)
-        Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged)
+    Q_OBJECT
+    Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
+    Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY
+                 typingUsersChanged)
+    Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged)
+    Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply)
+    Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit)
+    Q_PROPERTY(
+      bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
+    Q_PROPERTY(QString roomId READ roomId CONSTANT)
+    Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+    Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged)
+    Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
+    Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
+    Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
+    Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged)
+    Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
+    Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged)
+    Q_PROPERTY(bool isDirect READ isDirect NOTIFY isDirectChanged)
+    Q_PROPERTY(
+      QString directChatOtherUserId READ directChatOtherUserId NOTIFY directChatOtherUserIdChanged)
+    Q_PROPERTY(InputBar *input READ input CONSTANT)
+    Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged)
 
 public:
-        explicit TimelineModel(TimelineViewManager *manager,
-                               QString room_id,
-                               QObject *parent = nullptr);
-
-        enum Roles
-        {
-                Type,
-                TypeString,
-                IsOnlyEmoji,
-                Body,
-                FormattedBody,
-                PreviousMessageUserId,
-                IsSender,
-                UserId,
-                UserName,
-                PreviousMessageDay,
-                Day,
-                Timestamp,
-                Url,
-                ThumbnailUrl,
-                Blurhash,
-                Filename,
-                Filesize,
-                MimeType,
-                OriginalHeight,
-                OriginalWidth,
-                ProportionalHeight,
-                EventId,
-                State,
-                IsEdited,
-                IsEditable,
-                IsEncrypted,
-                Trustlevel,
-                EncryptionError,
-                ReplyTo,
-                Reactions,
-                RoomId,
-                RoomName,
-                RoomTopic,
-                CallType,
-                Dump,
-                RelatedEventCacheBuster,
-        };
-        Q_ENUM(Roles);
-
-        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;
-        QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
-        Q_INVOKABLE QVariant dataById(QString id, int role, QString relatedTo);
-
-        bool canFetchMore(const QModelIndex &) const override;
-        void fetchMore(const QModelIndex &) override;
-
-        Q_INVOKABLE QString displayName(QString id) const;
-        Q_INVOKABLE QString avatarUrl(QString id) const;
-        Q_INVOKABLE QString formatDateSeparator(QDate date) const;
-        Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, QColor bg);
-        Q_INVOKABLE QString formatMemberEvent(QString id);
-        Q_INVOKABLE QString formatJoinRuleEvent(QString id);
-        Q_INVOKABLE QString formatHistoryVisibilityEvent(QString id);
-        Q_INVOKABLE QString formatGuestAccessEvent(QString id);
-        Q_INVOKABLE QString formatPowerLevelEvent(QString id);
-
-        Q_INVOKABLE void viewRawMessage(QString id);
-        Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
-        Q_INVOKABLE void viewDecryptedRawMessage(QString id);
-        Q_INVOKABLE void openUserProfile(QString userid);
-        Q_INVOKABLE void editAction(QString id);
-        Q_INVOKABLE void replyAction(QString id);
-        Q_INVOKABLE void showReadReceipts(QString id);
-        Q_INVOKABLE void redactEvent(QString id);
-        Q_INVOKABLE int idToIndex(QString id) const;
-        Q_INVOKABLE QString indexToId(int index) const;
-        Q_INVOKABLE void openMedia(QString eventId);
-        Q_INVOKABLE void cacheMedia(QString eventId);
-        Q_INVOKABLE bool saveMedia(QString eventId) const;
-        Q_INVOKABLE void showEvent(QString eventId);
-        Q_INVOKABLE void copyLinkToEvent(QString eventId) const;
-        void cacheMedia(QString eventId, std::function<void(const QString filename)> callback);
-        Q_INVOKABLE void sendReset()
-        {
-                beginResetModel();
-                endResetModel();
-        }
-
-        Q_INVOKABLE void requestKeyForEvent(QString id);
-
-        std::vector<::Reaction> reactions(const std::string &event_id)
-        {
-                auto list = events.reactions(event_id);
-                std::vector<::Reaction> vec;
-                for (const auto &r : list)
-                        vec.push_back(r.value<Reaction>());
-                return vec;
-        }
-
-        void updateLastMessage();
-        void sync(const mtx::responses::JoinedRoom &room);
-        void addEvents(const mtx::responses::Timeline &events);
-        void syncState(const mtx::responses::State &state);
-        template<class T>
-        void sendMessageEvent(const T &content, mtx::events::EventType eventType);
-        RelatedInfo relatedInfo(QString id);
-
-        DescInfo lastMessage() const { return lastMessage_; }
-        bool isSpace() const { return isSpace_; }
-        bool isEncrypted() const { return isEncrypted_; }
-        crypto::Trust trustlevel() const;
-        int roomMemberCount() const;
+    explicit TimelineModel(TimelineViewManager *manager,
+                           QString room_id,
+                           QObject *parent = nullptr);
+
+    enum Roles
+    {
+        Type,
+        TypeString,
+        IsOnlyEmoji,
+        Body,
+        FormattedBody,
+        PreviousMessageUserId,
+        IsSender,
+        UserId,
+        UserName,
+        PreviousMessageDay,
+        Day,
+        Timestamp,
+        Url,
+        ThumbnailUrl,
+        Blurhash,
+        Filename,
+        Filesize,
+        MimeType,
+        OriginalHeight,
+        OriginalWidth,
+        ProportionalHeight,
+        EventId,
+        State,
+        IsEdited,
+        IsEditable,
+        IsEncrypted,
+        Trustlevel,
+        EncryptionError,
+        ReplyTo,
+        Reactions,
+        RoomId,
+        RoomName,
+        RoomTopic,
+        CallType,
+        Dump,
+        RelatedEventCacheBuster,
+    };
+    Q_ENUM(Roles);
+
+    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;
+    QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
+    Q_INVOKABLE QVariant dataById(QString id, int role, QString relatedTo);
+
+    bool canFetchMore(const QModelIndex &) const override;
+    void fetchMore(const QModelIndex &) override;
+
+    Q_INVOKABLE QString displayName(QString id) const;
+    Q_INVOKABLE QString avatarUrl(QString id) const;
+    Q_INVOKABLE QString formatDateSeparator(QDate date) const;
+    Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, QColor bg);
+    Q_INVOKABLE bool showAcceptKnockButton(QString id);
+    Q_INVOKABLE void acceptKnock(QString id);
+    Q_INVOKABLE QString formatMemberEvent(QString id);
+    Q_INVOKABLE QString formatJoinRuleEvent(QString id);
+    Q_INVOKABLE QString formatHistoryVisibilityEvent(QString id);
+    Q_INVOKABLE QString formatGuestAccessEvent(QString id);
+    Q_INVOKABLE QString formatPowerLevelEvent(QString id);
+
+    Q_INVOKABLE void viewRawMessage(QString id);
+    Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
+    Q_INVOKABLE void viewDecryptedRawMessage(QString id);
+    Q_INVOKABLE void openUserProfile(QString userid);
+    Q_INVOKABLE void editAction(QString id);
+    Q_INVOKABLE void replyAction(QString id);
+    Q_INVOKABLE void showReadReceipts(QString id);
+    Q_INVOKABLE void redactEvent(QString id);
+    Q_INVOKABLE int idToIndex(QString id) const;
+    Q_INVOKABLE QString indexToId(int index) const;
+    Q_INVOKABLE void openMedia(QString eventId);
+    Q_INVOKABLE void cacheMedia(QString eventId);
+    Q_INVOKABLE bool saveMedia(QString eventId) const;
+    Q_INVOKABLE void showEvent(QString eventId);
+    Q_INVOKABLE void copyLinkToEvent(QString eventId) const;
+    void cacheMedia(QString eventId, std::function<void(const QString filename)> callback);
+    Q_INVOKABLE void sendReset()
+    {
+        beginResetModel();
+        endResetModel();
+    }
+
+    Q_INVOKABLE void requestKeyForEvent(QString id);
+
+    std::vector<::Reaction> reactions(const std::string &event_id)
+    {
+        auto list = events.reactions(event_id);
+        std::vector<::Reaction> vec;
+        for (const auto &r : list)
+            vec.push_back(r.value<Reaction>());
+        return vec;
+    }
+
+    void updateLastMessage();
+    void sync(const mtx::responses::JoinedRoom &room);
+    void addEvents(const mtx::responses::Timeline &events);
+    void syncState(const mtx::responses::State &state);
+    template<class T>
+    void sendMessageEvent(const T &content, mtx::events::EventType eventType);
+    RelatedInfo relatedInfo(QString id);
+
+    DescInfo lastMessage() const { return lastMessage_; }
+    bool isSpace() const { return isSpace_; }
+    bool isEncrypted() const { return isEncrypted_; }
+    crypto::Trust trustlevel() const;
+    int roomMemberCount() const;
+    bool isDirect() const { return roomMemberCount() <= 2; }
+    QString directChatOtherUserId() const;
+
+    std::optional<mtx::events::collections::TimelineEvents> eventById(const QString &id)
+    {
+        auto e = events.get(id.toStdString(), "");
+        if (e)
+            return *e;
+        else
+            return std::nullopt;
+    }
 
 public slots:
-        void setCurrentIndex(int index);
-        int currentIndex() const { return idToIndex(currentId); }
-        void eventShown();
-        void markEventsAsRead(const std::vector<QString> &event_ids);
-        QVariantMap getDump(QString eventId, QString relatedTo) const;
-        void updateTypingUsers(const std::vector<QString> &users)
-        {
-                if (this->typingUsers_ != users) {
-                        this->typingUsers_ = users;
-                        emit typingUsersChanged(typingUsers_);
-                }
+    void setCurrentIndex(int index);
+    int currentIndex() const { return idToIndex(currentId); }
+    void eventShown();
+    void markEventsAsRead(const std::vector<QString> &event_ids);
+    QVariantMap getDump(QString eventId, QString relatedTo) const;
+    void updateTypingUsers(const std::vector<QString> &users)
+    {
+        if (this->typingUsers_ != users) {
+            this->typingUsers_ = users;
+            emit typingUsersChanged(typingUsers_);
         }
-        std::vector<QString> typingUsers() const { return typingUsers_; }
-        bool paginationInProgress() const { return m_paginationInProgress; }
-        QString reply() const { return reply_; }
-        void setReply(QString newReply)
-        {
-                if (edit_.startsWith('m'))
-                        return;
-
-                if (reply_ != newReply) {
-                        reply_ = newReply;
-                        emit replyChanged(reply_);
-                }
+    }
+    std::vector<QString> typingUsers() const { return typingUsers_; }
+    bool paginationInProgress() const { return m_paginationInProgress; }
+    QString reply() const { return reply_; }
+    void setReply(QString newReply)
+    {
+        if (edit_.startsWith('m'))
+            return;
+
+        if (reply_ != newReply) {
+            reply_ = newReply;
+            emit replyChanged(reply_);
         }
-        void resetReply()
-        {
-                if (!reply_.isEmpty()) {
-                        reply_ = "";
-                        emit replyChanged(reply_);
-                }
+    }
+    void resetReply()
+    {
+        if (!reply_.isEmpty()) {
+            reply_ = "";
+            emit replyChanged(reply_);
         }
-        QString edit() const { return edit_; }
-        void setEdit(QString newEdit);
-        void resetEdit();
-        void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
-        void clearTimeline() { events.clearTimeline(); }
-        void receivedSessionKey(const std::string &session_key)
-        {
-                events.receivedSessionKey(session_key);
-        }
-
-        QString roomName() const;
-        QString plainRoomName() const;
-        QString roomTopic() const;
-        InputBar *input() { return &input_; }
-        Permissions *permissions() { return &permissions_; }
-        QString roomAvatarUrl() const;
-        QString roomId() const { return room_id_; }
-
-        bool hasMentions() { return highlight_count > 0; }
-        int notificationCount() { return notification_count; }
-
-        QString scrollTarget() const;
+    }
+    QString edit() const { return edit_; }
+    void setEdit(QString newEdit);
+    void resetEdit();
+    void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
+    void clearTimeline() { events.clearTimeline(); }
+    void receivedSessionKey(const std::string &session_key)
+    {
+        events.receivedSessionKey(session_key);
+    }
+
+    QString roomName() const;
+    QString plainRoomName() const;
+    QString roomTopic() const;
+    InputBar *input() { return &input_; }
+    Permissions *permissions() { return &permissions_; }
+    QString roomAvatarUrl() const;
+    QString roomId() const { return room_id_; }
+
+    bool hasMentions() { return highlight_count > 0; }
+    int notificationCount() { return notification_count; }
+
+    QString scrollTarget() const;
 
 private slots:
-        void addPendingMessage(mtx::events::collections::TimelineEvents event);
-        void scrollTimerEvent();
+    void addPendingMessage(mtx::events::collections::TimelineEvents event);
+    void scrollTimerEvent();
 
 signals:
-        void currentIndexChanged(int index);
-        void redactionFailed(QString id);
-        void eventRedacted(QString id);
-        void mediaCached(QString mxcUrl, QString cacheUrl);
-        void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
-        void typingUsersChanged(std::vector<QString> users);
-        void replyChanged(QString reply);
-        void editChanged(QString reply);
-        void openReadReceiptsDialog(ReadReceiptsProxy *rr);
-        void showRawMessageDialog(QString rawMessage);
-        void paginationInProgressChanged(const bool);
-        void newCallEvent(const mtx::events::collections::TimelineEvents &event);
-        void scrollToIndex(int index);
-
-        void lastMessageChanged();
-        void notificationsChanged();
-
-        void newMessageToSend(mtx::events::collections::TimelineEvents event);
-        void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
-        void updateFlowEventId(std::string event_id);
-
-        void encryptionChanged();
-        void trustlevelChanged();
-        void roomNameChanged();
-        void plainRoomNameChanged();
-        void roomTopicChanged();
-        void roomAvatarUrlChanged();
-        void roomMemberCountChanged();
-        void permissionsChanged();
-        void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
-
-        void scrollTargetChanged();
+    void currentIndexChanged(int index);
+    void redactionFailed(QString id);
+    void eventRedacted(QString id);
+    void mediaCached(QString mxcUrl, QString cacheUrl);
+    void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
+    void typingUsersChanged(std::vector<QString> users);
+    void replyChanged(QString reply);
+    void editChanged(QString reply);
+    void openReadReceiptsDialog(ReadReceiptsProxy *rr);
+    void showRawMessageDialog(QString rawMessage);
+    void paginationInProgressChanged(const bool);
+    void newCallEvent(const mtx::events::collections::TimelineEvents &event);
+    void scrollToIndex(int index);
+
+    void lastMessageChanged();
+    void notificationsChanged();
+
+    void newMessageToSend(mtx::events::collections::TimelineEvents event);
+    void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
+    void updateFlowEventId(std::string event_id);
+
+    void encryptionChanged();
+    void trustlevelChanged();
+    void roomNameChanged();
+    void plainRoomNameChanged();
+    void roomTopicChanged();
+    void roomAvatarUrlChanged();
+    void roomMemberCountChanged();
+    void isDirectChanged();
+    void directChatOtherUserIdChanged();
+    void permissionsChanged();
+    void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
+
+    void scrollTargetChanged();
 
 private:
-        template<typename T>
-        void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType);
-        void readEvent(const std::string &id);
+    template<typename T>
+    void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType);
+    void readEvent(const std::string &id);
 
-        void setPaginationInProgress(const bool paginationInProgress);
+    void setPaginationInProgress(const bool paginationInProgress);
 
-        QSet<QString> read;
+    QSet<QString> read;
 
-        mutable EventStore events;
+    mutable EventStore events;
 
-        QString room_id_;
+    QString room_id_;
 
-        QString currentId, currentReadId;
-        QString reply_, edit_;
-        QString textBeforeEdit, replyBeforeEdit;
-        std::vector<QString> typingUsers_;
+    QString currentId, currentReadId;
+    QString reply_, edit_;
+    QString textBeforeEdit, replyBeforeEdit;
+    std::vector<QString> typingUsers_;
 
-        TimelineViewManager *manager_;
+    TimelineViewManager *manager_;
 
-        InputBar input_{this};
-        Permissions permissions_;
+    InputBar input_{this};
+    Permissions permissions_;
 
-        QTimer showEventTimer{this};
-        QString eventIdToShow;
-        int showEventTimerCounter = 0;
+    QTimer showEventTimer{this};
+    QString eventIdToShow;
+    int showEventTimerCounter = 0;
 
-        DescInfo lastMessage_{};
+    DescInfo lastMessage_{};
 
-        friend struct SendMessageVisitor;
+    friend struct SendMessageVisitor;
 
-        int notification_count = 0, highlight_count = 0;
+    int notification_count = 0, highlight_count = 0;
 
-        unsigned int relatedEventCacheBuster = 0;
+    unsigned int relatedEventCacheBuster = 0;
 
-        bool decryptDescription     = true;
-        bool m_paginationInProgress = false;
-        bool isSpace_               = false;
-        bool isEncrypted_           = false;
+    bool decryptDescription     = true;
+    bool m_paginationInProgress = false;
+    bool isSpace_               = false;
+    bool isEncrypted_           = false;
 };
 
 template<class T>
 void
 TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)
 {
-        if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) {
-                mtx::events::Sticker msgCopy = {};
-                msgCopy.content              = content;
-                msgCopy.type                 = eventType;
-                emit newMessageToSend(msgCopy);
-        } else {
-                mtx::events::RoomEvent<T> msgCopy = {};
-                msgCopy.content                   = content;
-                msgCopy.type                      = eventType;
-                emit newMessageToSend(msgCopy);
-        }
-        resetReply();
-        resetEdit();
+    if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) {
+        mtx::events::Sticker msgCopy = {};
+        msgCopy.content              = content;
+        msgCopy.type                 = eventType;
+        emit newMessageToSend(msgCopy);
+    } else {
+        mtx::events::RoomEvent<T> msgCopy = {};
+        msgCopy.content                   = content;
+        msgCopy.type                      = eventType;
+        emit newMessageToSend(msgCopy);
+    }
+    resetReply();
+    resetEdit();
 }
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 906e328ff32a3fec1fff33a3be666935cae96ce3..94e6a0d71ebee0b3d4ce49a4ef823a1ab63ce24f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -18,7 +18,6 @@
 #include "CombinedImagePackModel.h"
 #include "CompletionProxyModel.h"
 #include "DelegateChooser.h"
-#include "DeviceVerificationFlow.h"
 #include "EventAccessors.h"
 #include "ImagePackListModel.h"
 #include "InviteesModel.h"
@@ -27,6 +26,7 @@
 #include "MatrixClient.h"
 #include "MxcImageProvider.h"
 #include "ReadReceiptsModel.h"
+#include "RoomDirectoryModel.h"
 #include "RoomsModel.h"
 #include "SingleImagePackModel.h"
 #include "UserSettingsPage.h"
@@ -34,12 +34,18 @@
 #include "dialogs/ImageOverlay.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "encryption/DeviceVerificationFlow.h"
+#include "encryption/SelfVerificationStatus.h"
+#include "ui/MxcAnimatedImage.h"
+#include "ui/MxcMediaProxy.h"
 #include "ui/NhekoCursorShape.h"
 #include "ui/NhekoDropArea.h"
 #include "ui/NhekoGlobalObject.h"
+#include "ui/UIA.h"
 
 Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
 Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
+Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
 
 namespace msgs = mtx::events::msg;
 
@@ -63,73 +69,72 @@ template<typename T>
 static constexpr bool
 messageWithFileAndUrl(const mtx::events::Event<T> &)
 {
-        return is_detected<file_t, T>::value && is_detected<url_t, T>::value;
+    return is_detected<file_t, T>::value && is_detected<url_t, T>::value;
 }
 
 template<typename T>
 static constexpr void
 removeReplyFallback(mtx::events::Event<T> &e)
 {
-        if constexpr (is_detected<body_t, T>::value) {
-                if constexpr (std::is_same_v<std::optional<std::string>,
-                                             std::remove_cv_t<decltype(e.content.body)>>) {
-                        if (e.content.body) {
-                                e.content.body = utils::stripReplyFromBody(e.content.body);
-                        }
-                } else if constexpr (std::is_same_v<std::string,
-                                                    std::remove_cv_t<decltype(e.content.body)>>) {
-                        e.content.body = utils::stripReplyFromBody(e.content.body);
-                }
+    if constexpr (is_detected<body_t, T>::value) {
+        if constexpr (std::is_same_v<std::optional<std::string>,
+                                     std::remove_cv_t<decltype(e.content.body)>>) {
+            if (e.content.body) {
+                e.content.body = utils::stripReplyFromBody(e.content.body);
+            }
+        } else if constexpr (std::is_same_v<std::string,
+                                            std::remove_cv_t<decltype(e.content.body)>>) {
+            e.content.body = utils::stripReplyFromBody(e.content.body);
         }
+    }
 
-        if constexpr (is_detected<formatted_body_t, T>::value) {
-                if (e.content.format == "org.matrix.custom.html") {
-                        e.content.formatted_body =
-                          utils::stripReplyFromFormattedBody(e.content.formatted_body);
-                }
+    if constexpr (is_detected<formatted_body_t, T>::value) {
+        if (e.content.format == "org.matrix.custom.html") {
+            e.content.formatted_body = utils::stripReplyFromFormattedBody(e.content.formatted_body);
         }
+    }
 }
 }
 
 void
 TimelineViewManager::updateColorPalette()
 {
-        userColors.clear();
-
-        if (ChatPage::instance()->userSettings()->theme() == "light") {
-                view->rootContext()->setContextProperty("currentActivePalette", QPalette());
-                view->rootContext()->setContextProperty("currentInactivePalette", QPalette());
-        } else if (ChatPage::instance()->userSettings()->theme() == "dark") {
-                view->rootContext()->setContextProperty("currentActivePalette", QPalette());
-                view->rootContext()->setContextProperty("currentInactivePalette", QPalette());
-        } else {
-                view->rootContext()->setContextProperty("currentActivePalette", QPalette());
-                view->rootContext()->setContextProperty("currentInactivePalette", nullptr);
-        }
+    userColors.clear();
+
+    if (ChatPage::instance()->userSettings()->theme() == "light") {
+        view->rootContext()->setContextProperty("currentActivePalette", QPalette());
+        view->rootContext()->setContextProperty("currentInactivePalette", QPalette());
+    } else if (ChatPage::instance()->userSettings()->theme() == "dark") {
+        view->rootContext()->setContextProperty("currentActivePalette", QPalette());
+        view->rootContext()->setContextProperty("currentInactivePalette", QPalette());
+    } else {
+        view->rootContext()->setContextProperty("currentActivePalette", QPalette());
+        view->rootContext()->setContextProperty("currentInactivePalette", nullptr);
+    }
 }
 
 QColor
 TimelineViewManager::userColor(QString id, QColor background)
 {
-        if (!userColors.contains(id))
-                userColors.insert(id, QColor(utils::generateContrastingHexColor(id, background)));
-        return userColors.value(id);
+    if (!userColors.contains(id))
+        userColors.insert(id, QColor(utils::generateContrastingHexColor(id, background)));
+    return userColors.value(id);
 }
 
 QString
 TimelineViewManager::userPresence(QString id) const
 {
-        if (id.isEmpty())
-                return "";
-        else
-                return QString::fromStdString(
-                  mtx::presence::to_string(cache::presenceState(id.toStdString())));
+    if (id.isEmpty())
+        return "";
+    else
+        return QString::fromStdString(
+          mtx::presence::to_string(cache::presenceState(id.toStdString())));
 }
 
 QString
 TimelineViewManager::userStatus(QString id) const
 {
-        return QString::fromStdString(cache::statusMessage(id.toStdString()));
+    return QString::fromStdString(cache::statusMessage(id.toStdString()));
 }
 
 TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent)
@@ -137,449 +142,316 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , imgProvider(new MxcImageProvider())
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
-  , callManager_(callManager)
+  , jdenticonProvider(new JdenticonProvider())
   , rooms_(new RoomlistModel(this))
   , communities_(new CommunitiesModel(this))
-{
-        qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
-        qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
-        qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
-        qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
-        qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
-        qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
-        qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
-        qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
-        qRegisterMetaType<CombinedImagePackModel *>();
-
-        qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
-                                         "im.nheko",
-                                         1,
-                                         0,
-                                         "MtxEvent",
-                                         "Can't instantiate enum!");
-        qmlRegisterUncreatableMetaObject(
-          olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!");
-        qmlRegisterUncreatableMetaObject(
-          crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
-        qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
-                                         "im.nheko",
-                                         1,
-                                         0,
-                                         "VerificationStatus",
-                                         "Can't instantiate enum!");
-
-        qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
-        qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
-        qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
-        qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
-        qmlRegisterUncreatableType<DeviceVerificationFlow>(
-          "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
-        qmlRegisterUncreatableType<UserProfile>(
-          "im.nheko",
-          1,
-          0,
-          "UserProfileModel",
-          "UserProfile needs to be instantiated on the C++ side");
-        qmlRegisterUncreatableType<MemberList>(
-          "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side");
-        qmlRegisterUncreatableType<RoomSettings>(
-          "im.nheko",
-          1,
-          0,
-          "RoomSettingsModel",
-          "Room Settings needs to be instantiated on the C++ side");
-        qmlRegisterUncreatableType<TimelineModel>(
-          "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side");
-        qmlRegisterUncreatableType<ImagePackListModel>(
-          "im.nheko",
-          1,
-          0,
-          "ImagePackListModel",
-          "ImagePackListModel needs to be instantiated on the C++ side");
-        qmlRegisterUncreatableType<SingleImagePackModel>(
-          "im.nheko",
-          1,
-          0,
-          "SingleImagePackModel",
-          "SingleImagePackModel needs to be instantiated on the C++ side");
-        qmlRegisterUncreatableType<InviteesModel>(
-          "im.nheko",
-          1,
-          0,
-          "InviteesModel",
-          "InviteesModel needs to be instantiated on the C++ side");
-        qmlRegisterUncreatableType<ReadReceiptsProxy>(
-          "im.nheko",
-          1,
-          0,
-          "ReadReceiptsProxy",
-          "ReadReceiptsProxy needs to be instantiated on the C++ side");
-
-        static auto self = this;
-        qmlRegisterSingletonType<MainWindow>(
-          "im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = MainWindow::instance();
-                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-                  return ptr;
-          });
-        qmlRegisterSingletonType<TimelineViewManager>(
-          "im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = self;
-                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-                  return ptr;
-          });
-        qmlRegisterSingletonType<RoomlistModel>(
-          "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = new FilteredRoomlistModel(self->rooms_);
-
-                  connect(self->communities_,
-                          &CommunitiesModel::currentTagIdChanged,
-                          ptr,
-                          &FilteredRoomlistModel::updateFilterTag);
-                  connect(self->communities_,
-                          &CommunitiesModel::hiddenTagsChanged,
-                          ptr,
-                          &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
-                  return ptr;
-          });
-        qmlRegisterSingletonType<RoomlistModel>(
-          "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = self->communities_;
-                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-                  return ptr;
-          });
-        qmlRegisterSingletonType<UserSettings>(
-          "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = ChatPage::instance()->userSettings().data();
-                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-                  return ptr;
-          });
-        qmlRegisterSingletonType<CallManager>(
-          "im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = ChatPage::instance()->callManager();
-                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-                  return ptr;
-          });
-        qmlRegisterSingletonType<Clipboard>(
-          "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new Clipboard();
-          });
-        qmlRegisterSingletonType<Nheko>(
-          "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new Nheko();
-          });
-
-        qRegisterMetaType<mtx::events::collections::TimelineEvents>();
-        qRegisterMetaType<std::vector<DeviceInfo>>();
-
-        qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
-        qmlRegisterUncreatableType<emoji::Emoji>(
-          "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");
-        qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
-                                         "im.nheko.EmojiModel",
-                                         1,
-                                         0,
-                                         "EmojiCategory",
-                                         "Error: Only enums");
+  , callManager_(callManager)
+  , verificationManager_(new VerificationManager(this))
+{
+    qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
+    qRegisterMetaType<CombinedImagePackModel *>();
+
+    qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>();
+
+    qmlRegisterUncreatableMetaObject(
+      qml_mtx_events::staticMetaObject, "im.nheko", 1, 0, "MtxEvent", "Can't instantiate enum!");
+    qmlRegisterUncreatableMetaObject(
+      olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!");
+    qmlRegisterUncreatableMetaObject(
+      crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
+    qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
+                                     "im.nheko",
+                                     1,
+                                     0,
+                                     "VerificationStatus",
+                                     "Can't instantiate enum!");
+
+    qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
+    qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
+    qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
+    qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
+    qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
+    qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
+    qmlRegisterUncreatableType<DeviceVerificationFlow>(
+      "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
+    qmlRegisterUncreatableType<UserProfile>(
+      "im.nheko", 1, 0, "UserProfileModel", "UserProfile needs to be instantiated on the C++ side");
+    qmlRegisterUncreatableType<MemberList>(
+      "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side");
+    qmlRegisterUncreatableType<RoomSettings>(
+      "im.nheko",
+      1,
+      0,
+      "RoomSettingsModel",
+      "Room Settings needs to be instantiated on the C++ side");
+    qmlRegisterUncreatableType<TimelineModel>(
+      "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side");
+    qmlRegisterUncreatableType<ImagePackListModel>(
+      "im.nheko",
+      1,
+      0,
+      "ImagePackListModel",
+      "ImagePackListModel needs to be instantiated on the C++ side");
+    qmlRegisterUncreatableType<SingleImagePackModel>(
+      "im.nheko",
+      1,
+      0,
+      "SingleImagePackModel",
+      "SingleImagePackModel needs to be instantiated on the C++ side");
+    qmlRegisterUncreatableType<InviteesModel>(
+      "im.nheko", 1, 0, "InviteesModel", "InviteesModel needs to be instantiated on the C++ side");
+    qmlRegisterUncreatableType<ReadReceiptsProxy>(
+      "im.nheko",
+      1,
+      0,
+      "ReadReceiptsProxy",
+      "ReadReceiptsProxy needs to be instantiated on the C++ side");
+
+    static auto self = this;
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", MainWindow::instance());
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
+    qmlRegisterSingletonType<RoomlistModel>(
+      "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
+          auto ptr = new FilteredRoomlistModel(self->rooms_);
+
+          connect(self->communities_,
+                  &CommunitiesModel::currentTagIdChanged,
+                  ptr,
+                  &FilteredRoomlistModel::updateFilterTag);
+          connect(self->communities_,
+                  &CommunitiesModel::hiddenTagsChanged,
+                  ptr,
+                  &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
+          return ptr;
+      });
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
+    qmlRegisterSingletonInstance(
+      "im.nheko", 1, 0, "Settings", ChatPage::instance()->userSettings().data());
+    qmlRegisterSingletonInstance(
+      "im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
+    qmlRegisterSingletonType<Clipboard>(
+      "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
+          return new Clipboard();
+      });
+    qmlRegisterSingletonType<Nheko>(
+      "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
+          return new Nheko();
+      });
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "VerificationManager", verificationManager_);
+    qmlRegisterSingletonType<SelfVerificationStatus>(
+      "im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * {
+          return new SelfVerificationStatus();
+      });
+
+    qRegisterMetaType<mtx::events::collections::TimelineEvents>();
+    qRegisterMetaType<std::vector<DeviceInfo>>();
+
+    qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
+    qmlRegisterUncreatableType<emoji::Emoji>(
+      "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");
+    qmlRegisterUncreatableMetaObject(
+      emoji::staticMetaObject, "im.nheko.EmojiModel", 1, 0, "EmojiCategory", "Error: Only enums");
+
+    qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
 
 #ifdef USE_QUICK_VIEW
-        view      = new QQuickView(parent);
-        container = QWidget::createWindowContainer(view, parent);
+    view      = new QQuickView(parent);
+    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);
-        });
+    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);
-        updateColorPalette();
-        view->engine()->addImageProvider("MxcImage", imgProvider);
-        view->engine()->addImageProvider("colorimage", colorImgProvider);
-        view->engine()->addImageProvider("blurhash", blurhashProvider);
-        view->setSource(QUrl("qrc:///qml/Root.qml"));
-
-        connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
-        connect(
-          dynamic_cast<ChatPage *>(parent),
-          &ChatPage::receivedRoomDeviceVerificationRequest,
-          this,
-          [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
-                 TimelineModel *model) {
-                  if (this->isInitialSync_)
-                          return;
-
-                  auto event_id = QString::fromStdString(message.event_id);
-                  if (!this->dvList.contains(event_id)) {
-                          if (auto flow = DeviceVerificationFlow::NewInRoomVerification(
-                                this,
-                                model,
-                                message.content,
-                                QString::fromStdString(message.sender),
-                                event_id)) {
-                                  dvList[event_id] = flow;
-                                  emit newDeviceVerificationRequest(flow.data());
-                          }
-                  }
-          });
-        connect(dynamic_cast<ChatPage *>(parent),
-                &ChatPage::receivedDeviceVerificationRequest,
-                this,
-                [this](const mtx::events::msg::KeyVerificationRequest &msg, std::string sender) {
-                        if (this->isInitialSync_)
-                                return;
-
-                        if (!msg.transaction_id)
-                                return;
-
-                        auto txnid = QString::fromStdString(msg.transaction_id.value());
-                        if (!this->dvList.contains(txnid)) {
-                                if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
-                                      this, msg, QString::fromStdString(sender), txnid)) {
-                                        dvList[txnid] = flow;
-                                        emit newDeviceVerificationRequest(flow.data());
-                                }
-                        }
-                });
-        connect(dynamic_cast<ChatPage *>(parent),
-                &ChatPage::receivedDeviceVerificationStart,
-                this,
-                [this](const mtx::events::msg::KeyVerificationStart &msg, std::string sender) {
-                        if (this->isInitialSync_)
-                                return;
-
-                        if (!msg.transaction_id)
-                                return;
-
-                        auto txnid = QString::fromStdString(msg.transaction_id.value());
-                        if (!this->dvList.contains(txnid)) {
-                                if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
-                                      this, msg, QString::fromStdString(sender), txnid)) {
-                                        dvList[txnid] = flow;
-                                        emit newDeviceVerificationRequest(flow.data());
-                                }
-                        }
-                });
-        connect(parent, &ChatPage::loggedOut, this, [this]() {
-                isInitialSync_ = true;
-                emit initialSyncChanged(true);
-        });
-
-        connect(this,
-                &TimelineViewManager::openImageOverlayInternalCb,
-                this,
-                &TimelineViewManager::openImageOverlayInternal);
+    container->setMinimumSize(200, 200);
+    updateColorPalette();
+    view->engine()->addImageProvider("MxcImage", imgProvider);
+    view->engine()->addImageProvider("colorimage", colorImgProvider);
+    view->engine()->addImageProvider("blurhash", blurhashProvider);
+    if (JdenticonProvider::isAvailable())
+        view->engine()->addImageProvider("jdenticon", jdenticonProvider);
+    view->setSource(QUrl("qrc:///qml/Root.qml"));
+
+    connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
+    connect(dynamic_cast<ChatPage *>(parent),
+            &ChatPage::receivedRoomDeviceVerificationRequest,
+            verificationManager_,
+            &VerificationManager::receivedRoomDeviceVerificationRequest);
+    connect(dynamic_cast<ChatPage *>(parent),
+            &ChatPage::receivedDeviceVerificationRequest,
+            verificationManager_,
+            &VerificationManager::receivedDeviceVerificationRequest);
+    connect(dynamic_cast<ChatPage *>(parent),
+            &ChatPage::receivedDeviceVerificationStart,
+            verificationManager_,
+            &VerificationManager::receivedDeviceVerificationStart);
+    connect(parent, &ChatPage::loggedOut, this, [this]() {
+        isInitialSync_ = true;
+        emit initialSyncChanged(true);
+    });
+
+    connect(this,
+            &TimelineViewManager::openImageOverlayInternalCb,
+            this,
+            &TimelineViewManager::openImageOverlayInternal);
 }
 
 void
 TimelineViewManager::openRoomMembers(TimelineModel *room)
 {
-        if (!room)
-                return;
-        MemberList *memberList = new MemberList(room->roomId(), this);
-        emit openRoomMembersDialog(memberList, room);
+    if (!room)
+        return;
+    MemberList *memberList = new MemberList(room->roomId(), this);
+    emit openRoomMembersDialog(memberList, room);
 }
 
 void
 TimelineViewManager::openRoomSettings(QString room_id)
 {
-        RoomSettings *settings = new RoomSettings(room_id, this);
-        connect(rooms_->getRoomById(room_id).data(),
-                &TimelineModel::roomAvatarUrlChanged,
-                settings,
-                &RoomSettings::avatarChanged);
-        emit openRoomSettingsDialog(settings);
+    RoomSettings *settings = new RoomSettings(room_id, this);
+    connect(rooms_->getRoomById(room_id).data(),
+            &TimelineModel::roomAvatarUrlChanged,
+            settings,
+            &RoomSettings::avatarChanged);
+    emit openRoomSettingsDialog(settings);
 }
 
 void
 TimelineViewManager::openInviteUsers(QString roomId)
 {
-        InviteesModel *model = new InviteesModel{this};
-        connect(model, &InviteesModel::accept, this, [this, model, roomId]() {
-                emit inviteUsers(roomId, model->mxids());
-        });
-        emit openInviteUsersDialog(model);
+    InviteesModel *model = new InviteesModel{this};
+    connect(model, &InviteesModel::accept, this, [this, model, roomId]() {
+        emit inviteUsers(roomId, model->mxids());
+    });
+    emit openInviteUsersDialog(model);
 }
 
 void
 TimelineViewManager::openGlobalUserProfile(QString userId)
 {
-        UserProfile *profile = new UserProfile{QString{}, userId, this};
-        emit openProfile(profile);
+    UserProfile *profile = new UserProfile{QString{}, userId, this};
+    emit openProfile(profile);
 }
 
 void
 TimelineViewManager::setVideoCallItem()
 {
-        WebRTCSession::instance().setVideoItem(
-          view->rootObject()->findChild<QQuickItem *>("videoCallItem"));
+    WebRTCSession::instance().setVideoItem(
+      view->rootObject()->findChild<QQuickItem *>("videoCallItem"));
 }
 
 void
 TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
-        this->rooms_->sync(rooms_res);
-        this->communities_->sync(rooms_res);
+    this->rooms_->sync(rooms_res);
+    this->communities_->sync(rooms_res);
 
-        if (isInitialSync_) {
-                this->isInitialSync_ = false;
-                emit initialSyncChanged(false);
-        }
+    if (isInitialSync_) {
+        this->isInitialSync_ = false;
+        emit initialSyncChanged(false);
+    }
 }
 
 void
 TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
 {
-        if (auto room = rooms_->getRoomById(room_id)) {
-                if (rooms_->currentRoom() != room) {
-                        rooms_->setCurrentRoom(room_id);
-                        container->setFocus();
-                        nhlog::ui()->info("Activated room {}", room_id.toStdString());
-                }
-
-                room->showEvent(event_id);
+    if (auto room = rooms_->getRoomById(room_id)) {
+        if (rooms_->currentRoom() != room) {
+            rooms_->setCurrentRoom(room_id);
+            container->setFocus();
+            nhlog::ui()->info("Activated room {}", room_id.toStdString());
         }
+
+        room->showEvent(event_id);
+    }
 }
 
 QString
 TimelineViewManager::escapeEmoji(QString str) const
 {
-        return utils::replaceEmoji(str);
+    return utils::replaceEmoji(str);
 }
 
 void
 TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId)
 {
-        if (mxcUrl.isEmpty()) {
-                return;
-        }
+    if (mxcUrl.isEmpty()) {
+        return;
+    }
 
-        MxcImageProvider::download(
-          mxcUrl.remove("mxc://"), QSize(), [this, eventId](QString, QSize, QImage img, QString) {
-                  if (img.isNull()) {
-                          nhlog::ui()->error("Error when retrieving image for overlay.");
-                          return;
-                  }
+    MxcImageProvider::download(
+      mxcUrl.remove("mxc://"), QSize(), [this, eventId](QString, QSize, QImage img, QString) {
+          if (img.isNull()) {
+              nhlog::ui()->error("Error when retrieving image for overlay.");
+              return;
+          }
 
-                  emit openImageOverlayInternalCb(eventId, std::move(img));
-          });
+          emit openImageOverlayInternalCb(eventId, std::move(img));
+      });
 }
 
 void
 TimelineViewManager::openImagePackSettings(QString roomid)
 {
-        emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this));
+    emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this));
 }
 
 void
 TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
 {
-        auto pixmap = QPixmap::fromImage(img);
+    auto pixmap = QPixmap::fromImage(img);
 
-        auto imgDialog = new dialogs::ImageOverlay(pixmap);
-        imgDialog->showFullScreen();
+    auto imgDialog = new dialogs::ImageOverlay(pixmap);
+    imgDialog->showFullScreen();
 
-        auto room = rooms_->currentRoom();
-        connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() {
-                // hide the overlay while presenting the save dialog for better
-                // cross platform support.
-                imgDialog->hide();
+    auto room = rooms_->currentRoom();
+    connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() {
+        // hide the overlay while presenting the save dialog for better
+        // cross platform support.
+        imgDialog->hide();
 
-                if (!room->saveMedia(eventId)) {
-                        imgDialog->show();
-                } else {
-                        imgDialog->close();
-                }
-        });
-}
-
-void
-TimelineViewManager::openLeaveRoomDialog(QString roomid) const
-{
-        MainWindow::instance()->openLeaveRoomDialog(roomid);
-}
-
-void
-TimelineViewManager::verifyUser(QString userid)
-{
-        auto joined_rooms = cache::joinedRooms();
-        auto room_infos   = cache::getRoomInfo(joined_rooms);
-
-        for (std::string room_id : joined_rooms) {
-                if ((room_infos[QString::fromStdString(room_id)].member_count == 2) &&
-                    cache::isRoomEncrypted(room_id)) {
-                        auto room_members = cache::roomMembers(room_id);
-                        if (std::find(room_members.begin(),
-                                      room_members.end(),
-                                      (userid).toStdString()) != room_members.end()) {
-                                if (auto model =
-                                      rooms_->getRoomById(QString::fromStdString(room_id))) {
-                                        auto flow =
-                                          DeviceVerificationFlow::InitiateUserVerification(
-                                            this, model.data(), userid);
-                                        connect(model.data(),
-                                                &TimelineModel::updateFlowEventId,
-                                                this,
-                                                [this, flow](std::string eventId) {
-                                                        dvList[QString::fromStdString(eventId)] =
-                                                          flow;
-                                                });
-                                        emit newDeviceVerificationRequest(flow.data());
-                                        return;
-                                }
-                        }
-                }
-        }
-
-        emit ChatPage::instance()->showNotification(
-          tr("No encrypted private chat found with this user. Create an "
-             "encrypted private chat with this user and try again."));
-}
-
-void
-TimelineViewManager::removeVerificationFlow(DeviceVerificationFlow *flow)
-{
-        for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) {
-                if ((*it).second == flow) {
-                        dvList.remove((*it).first);
-                        return;
-                }
+        if (!room->saveMedia(eventId)) {
+            imgDialog->show();
+        } else {
+            imgDialog->close();
         }
-}
-
-void
-TimelineViewManager::verifyDevice(QString userid, QString deviceid)
-{
-        auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid);
-        this->dvList[flow->transactionId()] = flow;
-        emit newDeviceVerificationRequest(flow.data());
+    });
 }
 
 void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector<QString> &event_ids)
 {
-        if (auto room = rooms_->getRoomById(room_id)) {
-                room->markEventsAsRead(event_ids);
-        }
+    if (auto room = rooms_->getRoomById(room_id)) {
+        room->markEventsAsRead(event_ids);
+    }
 }
 
 void
 TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
 {
-        if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
-                room->receivedSessionKey(session_id);
-        }
+    if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
+        room->receivedSessionKey(session_id);
+    }
 }
 
 void
 TimelineViewManager::initializeRoomlist()
 {
-        rooms_->initializeRooms();
-        communities_->initializeSidebar();
+    rooms_->initializeRooms();
+    communities_->initializeSidebar();
 }
 
 void
@@ -587,178 +459,175 @@ TimelineViewManager::queueReply(const QString &roomid,
                                 const QString &repliedToEvent,
                                 const QString &replyBody)
 {
-        if (auto room = rooms_->getRoomById(roomid)) {
-                room->setReply(repliedToEvent);
-                room->input()->message(replyBody);
-        }
+    if (auto room = rooms_->getRoomById(roomid)) {
+        room->setReply(repliedToEvent);
+        room->input()->message(replyBody);
+    }
 }
 
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallInvite &callInvite)
 {
-        if (auto room = rooms_->getRoomById(roomid))
-                room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
+    if (auto room = rooms_->getRoomById(roomid))
+        room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
 }
 
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallCandidates &callCandidates)
 {
-        if (auto room = rooms_->getRoomById(roomid))
-                room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
+    if (auto room = rooms_->getRoomById(roomid))
+        room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
 }
 
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallAnswer &callAnswer)
 {
-        if (auto room = rooms_->getRoomById(roomid))
-                room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
+    if (auto room = rooms_->getRoomById(roomid))
+        room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
 }
 
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallHangUp &callHangUp)
 {
-        if (auto room = rooms_->getRoomById(roomid))
-                room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
+    if (auto room = rooms_->getRoomById(roomid))
+        room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
 }
 
 void
 TimelineViewManager::focusMessageInput()
 {
-        emit focusInput();
+    emit focusInput();
 }
 
 QObject *
 TimelineViewManager::completerFor(QString completerName, QString roomId)
 {
-        if (completerName == "user") {
-                auto userModel = new UsersModel(roomId.toStdString());
-                auto proxy     = new CompletionProxyModel(userModel);
-                userModel->setParent(proxy);
-                return proxy;
-        } else if (completerName == "emoji") {
-                auto emojiModel = new emoji::EmojiModel();
-                auto proxy      = new CompletionProxyModel(emojiModel);
-                emojiModel->setParent(proxy);
-                return proxy;
-        } else if (completerName == "allemoji") {
-                auto emojiModel = new emoji::EmojiModel();
-                auto proxy = new CompletionProxyModel(emojiModel, 1, static_cast<size_t>(-1) / 4);
-                emojiModel->setParent(proxy);
-                return proxy;
-        } else if (completerName == "room") {
-                auto roomModel = new RoomsModel(false);
-                auto proxy     = new CompletionProxyModel(roomModel, 4);
-                roomModel->setParent(proxy);
-                return proxy;
-        } else if (completerName == "roomAliases") {
-                auto roomModel = new RoomsModel(true);
-                auto proxy     = new CompletionProxyModel(roomModel);
-                roomModel->setParent(proxy);
-                return proxy;
-        } else if (completerName == "stickers") {
-                auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true);
-                auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4);
-                stickerModel->setParent(proxy);
-                return proxy;
-        }
-        return nullptr;
+    if (completerName == "user") {
+        auto userModel = new UsersModel(roomId.toStdString());
+        auto proxy     = new CompletionProxyModel(userModel);
+        userModel->setParent(proxy);
+        return proxy;
+    } else if (completerName == "emoji") {
+        auto emojiModel = new emoji::EmojiModel();
+        auto proxy      = new CompletionProxyModel(emojiModel);
+        emojiModel->setParent(proxy);
+        return proxy;
+    } else if (completerName == "allemoji") {
+        auto emojiModel = new emoji::EmojiModel();
+        auto proxy      = new CompletionProxyModel(emojiModel, 1, static_cast<size_t>(-1) / 4);
+        emojiModel->setParent(proxy);
+        return proxy;
+    } else if (completerName == "room") {
+        auto roomModel = new RoomsModel(false);
+        auto proxy     = new CompletionProxyModel(roomModel, 4);
+        roomModel->setParent(proxy);
+        return proxy;
+    } else if (completerName == "roomAliases") {
+        auto roomModel = new RoomsModel(true);
+        auto proxy     = new CompletionProxyModel(roomModel);
+        roomModel->setParent(proxy);
+        return proxy;
+    } else if (completerName == "stickers") {
+        auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true);
+        auto proxy        = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4);
+        stickerModel->setParent(proxy);
+        return proxy;
+    }
+    return nullptr;
 }
 
 void
 TimelineViewManager::focusTimeline()
 {
-        getWidget()->setFocus();
+    getWidget()->setFocus();
 }
 
 void
 TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
                                           QString roomId)
 {
-        auto room                                                = rooms_->getRoomById(roomId);
-        auto content                                             = mtx::accessors::url(*e);
-        std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e);
-
-        if (encryptionInfo) {
-                http::client()->download(
-                  content,
-                  [this, roomId, e, encryptionInfo](const std::string &res,
-                                                    const std::string &content_type,
-                                                    const std::string &originalFilename,
-                                                    mtx::http::RequestErr err) {
-                          if (err)
-                                  return;
-
-                          auto data = mtx::crypto::to_string(
-                            mtx::crypto::decrypt_file(res, encryptionInfo.value()));
-
-                          http::client()->upload(
-                            data,
-                            content_type,
-                            originalFilename,
-                            [this, roomId, e](const mtx::responses::ContentURI &res,
-                                              mtx::http::RequestErr err) mutable {
-                                    if (err) {
-                                            nhlog::net()->warn("failed to upload media: {} {} ({})",
-                                                               err->matrix_error.error,
-                                                               to_string(err->matrix_error.errcode),
-                                                               static_cast<int>(err->status_code));
-                                            return;
-                                    }
-
-                                    std::visit(
-                                      [this, roomId, url = res.content_uri](auto ev) {
-                                              if constexpr (mtx::events::message_content_to_type<
-                                                              decltype(ev.content)> ==
-                                                            mtx::events::EventType::RoomMessage) {
-                                                      if constexpr (messageWithFileAndUrl(ev)) {
-                                                              ev.content.relations.relations
-                                                                .clear();
-                                                              ev.content.file.reset();
-                                                              ev.content.url = url;
-                                                      }
-
-                                                      if (auto room = rooms_->getRoomById(roomId)) {
-                                                              removeReplyFallback(ev);
-                                                              ev.content.relations.relations
-                                                                .clear();
-                                                              room->sendMessageEvent(
-                                                                ev.content,
-                                                                mtx::events::EventType::
-                                                                  RoomMessage);
-                                                      }
-                                              }
-                                      },
-                                      *e);
-                            });
-
-                          return;
-                  });
-
-                return;
-        }
+    auto room                                                = rooms_->getRoomById(roomId);
+    auto content                                             = mtx::accessors::url(*e);
+    std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e);
+
+    if (encryptionInfo) {
+        http::client()->download(
+          content,
+          [this, roomId, e, encryptionInfo](const std::string &res,
+                                            const std::string &content_type,
+                                            const std::string &originalFilename,
+                                            mtx::http::RequestErr err) {
+              if (err)
+                  return;
+
+              auto data =
+                mtx::crypto::to_string(mtx::crypto::decrypt_file(res, encryptionInfo.value()));
+
+              http::client()->upload(
+                data,
+                content_type,
+                originalFilename,
+                [this, roomId, e](const mtx::responses::ContentURI &res,
+                                  mtx::http::RequestErr err) mutable {
+                    if (err) {
+                        nhlog::net()->warn("failed to upload media: {} {} ({})",
+                                           err->matrix_error.error,
+                                           to_string(err->matrix_error.errcode),
+                                           static_cast<int>(err->status_code));
+                        return;
+                    }
+
+                    std::visit(
+                      [this, roomId, url = res.content_uri](auto ev) {
+                          using namespace mtx::events;
+                          if constexpr (EventType::RoomMessage ==
+                                          message_content_to_type<decltype(ev.content)> ||
+                                        EventType::Sticker ==
+                                          message_content_to_type<decltype(ev.content)>) {
+                              if constexpr (messageWithFileAndUrl(ev)) {
+                                  ev.content.relations.relations.clear();
+                                  ev.content.file.reset();
+                                  ev.content.url = url;
+                              }
+
+                              if (auto room = rooms_->getRoomById(roomId)) {
+                                  removeReplyFallback(ev);
+                                  ev.content.relations.relations.clear();
+                                  room->sendMessageEvent(ev.content,
+                                                         mtx::events::EventType::RoomMessage);
+                              }
+                          }
+                      },
+                      *e);
+                });
 
-        std::visit(
-          [room](auto e) {
-                  if constexpr (mtx::events::message_content_to_type<decltype(e.content)> ==
-                                mtx::events::EventType::RoomMessage) {
-                          e.content.relations.relations.clear();
-                          removeReplyFallback(e);
-                          room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage);
-                  }
-          },
-          *e);
+              return;
+          });
+
+        return;
+    }
+
+    std::visit(
+      [room](auto e) {
+          if constexpr (mtx::events::message_content_to_type<decltype(e.content)> ==
+                        mtx::events::EventType::RoomMessage) {
+              e.content.relations.relations.clear();
+              removeReplyFallback(e);
+              room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage);
+          }
+      },
+      *e);
 }
 
 //! WORKAROUND(Nico): for https://bugreports.qt.io/browse/QTBUG-93281
 void
 TimelineViewManager::fixImageRendering(QQuickTextDocument *t, QQuickItem *i)
 {
-        if (t) {
-                QObject::connect(
-                  t->textDocument(), SIGNAL(imagesLoaded()), i, SLOT(updateWholeDocument()));
-        }
+    if (t) {
+        QObject::connect(t->textDocument(), SIGNAL(imagesLoaded()), i, SLOT(updateWholeDocument()));
+    }
 }
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 4dd5e996ba286c46da54df36fad8c30fec8a94ac..6696b1c4dfb756a87500f6f5864771a51ec34808 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -9,7 +9,6 @@
 #include <QQuickTextDocument>
 #include <QQuickView>
 #include <QQuickWidget>
-#include <QSharedPointer>
 #include <QWidget>
 
 #include <mtx/common.hpp>
@@ -17,142 +16,138 @@
 #include <mtx/responses/sync.hpp>
 
 #include "Cache.h"
-#include "CallManager.h"
+#include "JdenticonProvider.h"
 #include "Logging.h"
 #include "TimelineModel.h"
 #include "Utils.h"
-#include "WebRTCSession.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "encryption/VerificationManager.h"
 #include "timeline/CommunitiesModel.h"
 #include "timeline/RoomlistModel.h"
+#include "voip/CallManager.h"
+#include "voip/WebRTCSession.h"
 
 class MxcImageProvider;
 class BlurhashProvider;
 class ColorImageProvider;
 class UserSettings;
 class ChatPage;
-class DeviceVerificationFlow;
 class ImagePackListModel;
 
 class TimelineViewManager : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(
-          bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
-        Q_PROPERTY(
-          bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged)
+    Q_PROPERTY(
+      bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
+    Q_PROPERTY(
+      bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged)
 
 public:
-        TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr);
-        QWidget *getWidget() const { return container; }
+    TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr);
+    QWidget *getWidget() const { return container; }
 
-        void sync(const mtx::responses::Rooms &rooms);
+    void sync(const mtx::responses::Rooms &rooms);
 
-        MxcImageProvider *imageProvider() { return imgProvider; }
-        CallManager *callManager() { return callManager_; }
+    MxcImageProvider *imageProvider() { return imgProvider; }
+    CallManager *callManager() { return callManager_; }
+    VerificationManager *verificationManager() { return verificationManager_; }
 
-        void clearAll() { rooms_->clear(); }
+    void clearAll() { rooms_->clear(); }
 
-        Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
-        bool isWindowFocused() const { return isWindowFocused_; }
-        Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId);
-        Q_INVOKABLE void openImagePackSettings(QString roomid);
-        Q_INVOKABLE QColor userColor(QString id, QColor background);
-        Q_INVOKABLE QString escapeEmoji(QString str) const;
-        Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }
+    Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
+    bool isWindowFocused() const { return isWindowFocused_; }
+    Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId);
+    Q_INVOKABLE void openImagePackSettings(QString roomid);
+    Q_INVOKABLE QColor userColor(QString id, QColor background);
+    Q_INVOKABLE QString escapeEmoji(QString str) const;
+    Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }
 
-        Q_INVOKABLE QString userPresence(QString id) const;
-        Q_INVOKABLE QString userStatus(QString id) const;
+    Q_INVOKABLE QString userPresence(QString id) const;
+    Q_INVOKABLE QString userStatus(QString id) const;
 
-        Q_INVOKABLE void openRoomMembers(TimelineModel *room);
-        Q_INVOKABLE void openRoomSettings(QString room_id);
-        Q_INVOKABLE void openInviteUsers(QString roomId);
-        Q_INVOKABLE void openGlobalUserProfile(QString userId);
+    Q_INVOKABLE void openRoomMembers(TimelineModel *room);
+    Q_INVOKABLE void openRoomSettings(QString room_id);
+    Q_INVOKABLE void openInviteUsers(QString roomId);
+    Q_INVOKABLE void openGlobalUserProfile(QString userId);
 
-        Q_INVOKABLE void focusMessageInput();
-        Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
-        Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
+    Q_INVOKABLE void focusMessageInput();
 
-        Q_INVOKABLE void fixImageRendering(QQuickTextDocument *t, QQuickItem *i);
-
-        void verifyUser(QString userid);
-        void verifyDevice(QString userid, QString deviceid);
+    Q_INVOKABLE void fixImageRendering(QQuickTextDocument *t, QQuickItem *i);
 
 signals:
-        void activeTimelineChanged(TimelineModel *timeline);
-        void initialSyncChanged(bool isInitialSync);
-        void replyingEventChanged(QString replyingEvent);
-        void replyClosed();
-        void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
-        void inviteUsers(QString roomId, QStringList users);
-        void showRoomList();
-        void narrowViewChanged();
-        void focusChanged();
-        void focusInput();
-        void openImageOverlayInternalCb(QString eventId, QImage img);
-        void openRoomMembersDialog(MemberList *members, TimelineModel *room);
-        void openRoomSettingsDialog(RoomSettings *settings);
-        void openInviteUsersDialog(InviteesModel *invitees);
-        void openProfile(UserProfile *profile);
-        void showImagePackSettings(ImagePackListModel *packlist);
+    void activeTimelineChanged(TimelineModel *timeline);
+    void initialSyncChanged(bool isInitialSync);
+    void replyingEventChanged(QString replyingEvent);
+    void replyClosed();
+    void inviteUsers(QString roomId, QStringList users);
+    void showRoomList();
+    void narrowViewChanged();
+    void focusChanged();
+    void focusInput();
+    void openImageOverlayInternalCb(QString eventId, QImage img);
+    void openRoomMembersDialog(MemberList *members, TimelineModel *room);
+    void openRoomSettingsDialog(RoomSettings *settings);
+    void openInviteUsersDialog(InviteesModel *invitees);
+    void openProfile(UserProfile *profile);
+    void showImagePackSettings(ImagePackListModel *packlist);
+    void openLeaveRoomDialog(QString roomid);
 
 public slots:
-        void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
-        void receivedSessionKey(const std::string &room_id, const std::string &session_id);
-        void initializeRoomlist();
-        void chatFocusChanged(bool focused)
-        {
-                isWindowFocused_ = focused;
-                emit focusChanged();
-        }
-
-        void showEvent(const QString &room_id, const QString &event_id);
-        void focusTimeline();
-
-        void updateColorPalette();
-        void queueReply(const QString &roomid,
-                        const QString &repliedToEvent,
-                        const QString &replyBody);
-        void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
-        void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
-        void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
-        void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
-
-        void setVideoCallItem();
-
-        QObject *completerFor(QString completerName, QString roomId = "");
-        void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
-
-        RoomlistModel *rooms() { return rooms_; }
+    void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
+    void receivedSessionKey(const std::string &room_id, const std::string &session_id);
+    void initializeRoomlist();
+    void chatFocusChanged(bool focused)
+    {
+        isWindowFocused_ = focused;
+        emit focusChanged();
+    }
+
+    void showEvent(const QString &room_id, const QString &event_id);
+    void focusTimeline();
+
+    void updateColorPalette();
+    void queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody);
+    void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
+    void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
+    void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
+    void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
+
+    void setVideoCallItem();
+
+    QObject *completerFor(QString completerName, QString roomId = "");
+    void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
+
+    RoomlistModel *rooms() { return rooms_; }
 
 private slots:
-        void openImageOverlayInternal(QString eventId, QImage img);
+    void openImageOverlayInternal(QString eventId, QImage img);
 
 private:
 #ifdef USE_QUICK_VIEW
-        QQuickView *view;
+    QQuickView *view;
 #else
-        QQuickWidget *view;
+    QQuickWidget *view;
 #endif
-        QWidget *container;
-
-        MxcImageProvider *imgProvider;
-        ColorImageProvider *colorImgProvider;
-        BlurhashProvider *blurhashProvider;
+    QWidget *container;
 
-        CallManager *callManager_ = nullptr;
+    MxcImageProvider *imgProvider;
+    ColorImageProvider *colorImgProvider;
+    BlurhashProvider *blurhashProvider;
+    JdenticonProvider *jdenticonProvider;
 
-        bool isInitialSync_   = true;
-        bool isWindowFocused_ = false;
+    bool isInitialSync_   = true;
+    bool isWindowFocused_ = false;
 
-        RoomlistModel *rooms_          = nullptr;
-        CommunitiesModel *communities_ = nullptr;
+    RoomlistModel *rooms_          = nullptr;
+    CommunitiesModel *communities_ = nullptr;
 
-        QHash<QString, QColor> userColors;
+    // don't move this above the rooms_
+    CallManager *callManager_                 = nullptr;
+    VerificationManager *verificationManager_ = nullptr;
 
-        QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;
+    QHash<QString, QColor> userColors;
 };
 Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationAccept)
 Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationCancel)
diff --git a/src/ui/Badge.cpp b/src/ui/Badge.cpp
index 66210d06d5cb0e192de3c80000d03a45c1ea1024..1b5aba2faaf796b48a048e064590a67d0b6c9c27 100644
--- a/src/ui/Badge.cpp
+++ b/src/ui/Badge.cpp
@@ -9,214 +9,214 @@
 Badge::Badge(QWidget *parent)
   : OverlayWidget(parent)
 {
-        init();
+    init();
 }
 
 Badge::Badge(const QIcon &icon, QWidget *parent)
   : OverlayWidget(parent)
 {
-        init();
-        setIcon(icon);
+    init();
+    setIcon(icon);
 }
 
 Badge::Badge(const QString &text, QWidget *parent)
   : OverlayWidget(parent)
 {
-        init();
-        setText(text);
+    init();
+    setText(text);
 }
 
 void
 Badge::init()
 {
-        x_ = 0;
-        y_ = 0;
-        // TODO: Make padding configurable.
-        padding_  = 5;
-        diameter_ = 24;
+    x_ = 0;
+    y_ = 0;
+    // TODO: Make padding configurable.
+    padding_  = 5;
+    diameter_ = 24;
 
-        setAttribute(Qt::WA_TransparentForMouseEvents);
+    setAttribute(Qt::WA_TransparentForMouseEvents);
 
-        QFont _font(font());
-        _font.setPointSizeF(7.5);
-        _font.setStyleName("Bold");
+    QFont _font(font());
+    _font.setPointSizeF(7.5);
+    _font.setStyleName("Bold");
 
-        setFont(_font);
-        setText("");
+    setFont(_font);
+    setText("");
 }
 
 QString
 Badge::text() const
 {
-        return text_;
+    return text_;
 }
 
 QIcon
 Badge::icon() const
 {
-        return icon_;
+    return icon_;
 }
 
 QSize
 Badge::sizeHint() const
 {
-        const int d = diameter();
-        return QSize(d + 4, d + 4);
+    const int d = diameter();
+    return QSize(d + 4, d + 4);
 }
 
 qreal
 Badge::relativeYPosition() const
 {
-        return y_;
+    return y_;
 }
 
 qreal
 Badge::relativeXPosition() const
 {
-        return x_;
+    return x_;
 }
 
 QPointF
 Badge::relativePosition() const
 {
-        return QPointF(x_, y_);
+    return QPointF(x_, y_);
 }
 
 QColor
 Badge::backgroundColor() const
 {
-        if (!background_color_.isValid())
-                return QColor("black");
+    if (!background_color_.isValid())
+        return QColor("black");
 
-        return background_color_;
+    return background_color_;
 }
 
 QColor
 Badge::textColor() const
 {
-        if (!text_color_.isValid())
-                return QColor("white");
+    if (!text_color_.isValid())
+        return QColor("white");
 
-        return text_color_;
+    return text_color_;
 }
 
 void
 Badge::setTextColor(const QColor &color)
 {
-        text_color_ = color;
+    text_color_ = color;
 }
 
 void
 Badge::setBackgroundColor(const QColor &color)
 {
-        background_color_ = color;
+    background_color_ = color;
 }
 
 void
 Badge::setRelativePosition(const QPointF &pos)
 {
-        setRelativePosition(pos.x(), pos.y());
+    setRelativePosition(pos.x(), pos.y());
 }
 
 void
 Badge::setRelativePosition(qreal x, qreal y)
 {
-        x_ = x;
-        y_ = y;
-        update();
+    x_ = x;
+    y_ = y;
+    update();
 }
 
 void
 Badge::setRelativeXPosition(qreal x)
 {
-        x_ = x;
-        update();
+    x_ = x;
+    update();
 }
 
 void
 Badge::setRelativeYPosition(qreal y)
 {
-        y_ = y;
-        update();
+    y_ = y;
+    update();
 }
 
 void
 Badge::setIcon(const QIcon &icon)
 {
-        icon_ = icon;
-        update();
+    icon_ = icon;
+    update();
 }
 
 void
 Badge::setText(const QString &text)
 {
-        text_ = text;
+    text_ = text;
 
-        if (!icon_.isNull())
-                icon_ = QIcon();
+    if (!icon_.isNull())
+        icon_ = QIcon();
 
-        size_ = fontMetrics().size(Qt::TextShowMnemonic, text);
+    size_ = fontMetrics().size(Qt::TextShowMnemonic, text);
 
-        update();
+    update();
 }
 
 void
 Badge::setDiameter(int diameter)
 {
-        if (diameter > 0) {
-                diameter_ = diameter;
-                update();
-        }
+    if (diameter > 0) {
+        diameter_ = diameter;
+        update();
+    }
 }
 
 void
 Badge::paintEvent(QPaintEvent *)
 {
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-        painter.translate(x_, y_);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
+    painter.translate(x_, y_);
 
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
+    QBrush brush;
+    brush.setStyle(Qt::SolidPattern);
 
-        painter.setBrush(brush);
-        painter.setPen(Qt::NoPen);
+    painter.setBrush(brush);
+    painter.setPen(Qt::NoPen);
 
-        const int d = diameter();
+    const int d = diameter();
 
-        QRectF r(0, 0, d, d);
-        r.translate(QPointF((width() - d), (height() - d)) / 2);
+    QRectF r(0, 0, d, d);
+    r.translate(QPointF((width() - d), (height() - d)) / 2);
 
-        if (icon_.isNull()) {
-                QPen pen;
-                // TODO: Make badge width configurable.
-                pen.setWidth(1);
-                pen.setColor(textColor());
+    if (icon_.isNull()) {
+        QPen pen;
+        // TODO: Make badge width configurable.
+        pen.setWidth(1);
+        pen.setColor(textColor());
 
-                painter.setPen(pen);
-                painter.drawEllipse(r);
+        painter.setPen(pen);
+        painter.drawEllipse(r);
 
-                painter.setPen(textColor());
-                painter.setBrush(Qt::NoBrush);
-                painter.drawText(r.translated(0, -0.5), Qt::AlignCenter, text_);
-        } else {
-                painter.drawEllipse(r);
-                QRectF q(0, 0, 16, 16);
-                q.moveCenter(r.center());
-                QPixmap pixmap = icon().pixmap(16, 16);
-                QPainter icon(&pixmap);
-                icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
-                icon.fillRect(pixmap.rect(), textColor());
-                painter.drawPixmap(q.toRect(), pixmap);
-        }
+        painter.setPen(textColor());
+        painter.setBrush(Qt::NoBrush);
+        painter.drawText(r.translated(0, -0.5), Qt::AlignCenter, text_);
+    } else {
+        painter.drawEllipse(r);
+        QRectF q(0, 0, 16, 16);
+        q.moveCenter(r.center());
+        QPixmap pixmap = icon().pixmap(16, 16);
+        QPainter icon(&pixmap);
+        icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
+        icon.fillRect(pixmap.rect(), textColor());
+        painter.drawPixmap(q.toRect(), pixmap);
+    }
 }
 
 int
 Badge::diameter() const
 {
-        if (icon_.isNull()) {
-                return qMax(size_.width(), size_.height()) + padding_;
-        }
+    if (icon_.isNull()) {
+        return qMax(size_.width(), size_.height()) + padding_;
+    }
 
-        return diameter_;
+    return diameter_;
 }
diff --git a/src/ui/Badge.h b/src/ui/Badge.h
index 98e1687364d9dee91131a0b1037f5e226fc0c206..15b69b585d953ad234c4a863e4298fc26e0f9c1d 100644
--- a/src/ui/Badge.h
+++ b/src/ui/Badge.h
@@ -13,54 +13,54 @@
 
 class Badge : public OverlayWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-        Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition)
+    Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
+    Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
+    Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition)
 
 public:
-        explicit Badge(QWidget *parent = nullptr);
-        explicit Badge(const QIcon &icon, QWidget *parent = nullptr);
-        explicit Badge(const QString &text, QWidget *parent = nullptr);
+    explicit Badge(QWidget *parent = nullptr);
+    explicit Badge(const QIcon &icon, QWidget *parent = nullptr);
+    explicit Badge(const QString &text, QWidget *parent = nullptr);
 
-        void setBackgroundColor(const QColor &color);
-        void setTextColor(const QColor &color);
-        void setIcon(const QIcon &icon);
-        void setRelativePosition(const QPointF &pos);
-        void setRelativePosition(qreal x, qreal y);
-        void setRelativeXPosition(qreal x);
-        void setRelativeYPosition(qreal y);
-        void setText(const QString &text);
-        void setDiameter(int diameter);
+    void setBackgroundColor(const QColor &color);
+    void setTextColor(const QColor &color);
+    void setIcon(const QIcon &icon);
+    void setRelativePosition(const QPointF &pos);
+    void setRelativePosition(qreal x, qreal y);
+    void setRelativeXPosition(qreal x);
+    void setRelativeYPosition(qreal y);
+    void setText(const QString &text);
+    void setDiameter(int diameter);
 
-        QIcon icon() const;
-        QString text() const;
-        QColor backgroundColor() const;
-        QColor textColor() const;
-        QPointF relativePosition() const;
-        QSize sizeHint() const override;
-        qreal relativeXPosition() const;
-        qreal relativeYPosition() const;
+    QIcon icon() const;
+    QString text() const;
+    QColor backgroundColor() const;
+    QColor textColor() const;
+    QPointF relativePosition() const;
+    QSize sizeHint() const override;
+    qreal relativeXPosition() const;
+    qreal relativeYPosition() const;
 
-        int diameter() const;
+    int diameter() const;
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 private:
-        void init();
+    void init();
 
-        QColor background_color_;
-        QColor text_color_;
+    QColor background_color_;
+    QColor text_color_;
 
-        QIcon icon_;
-        QSize size_;
-        QString text_;
+    QIcon icon_;
+    QSize size_;
+    QString text_;
 
-        int padding_;
-        int diameter_;
+    int padding_;
+    int diameter_;
 
-        qreal x_;
-        qreal y_;
+    qreal x_;
+    qreal y_;
 };
diff --git a/src/ui/DropShadow.cpp b/src/ui/DropShadow.cpp
index a413e3f738efd3f82ed482cf66961b8ec597bb29..31d13b9369e1fa23404041d7624d78a5683ccf55 100644
--- a/src/ui/DropShadow.cpp
+++ b/src/ui/DropShadow.cpp
@@ -19,94 +19,89 @@ DropShadow::draw(QPainter &painter,
                  qreal width,
                  qreal height)
 {
-        painter.setPen(Qt::NoPen);
+    painter.setPen(Qt::NoPen);
 
-        QLinearGradient gradient;
-        gradient.setColorAt(startPosition, start);
-        gradient.setColorAt(endPosition0, end);
+    QLinearGradient gradient;
+    gradient.setColorAt(startPosition, start);
+    gradient.setColorAt(endPosition0, end);
 
-        // Right
-        QPointF right0(width - margin, height / 2);
-        QPointF right1(width, height / 2);
-        gradient.setStart(right0);
-        gradient.setFinalStop(right1);
-        painter.setBrush(QBrush(gradient));
-        // Deprecated in 5.13: painter.drawRoundRect(
-        //  QRectF(QPointF(width - margin * radius, margin), QPointF(width, height -
-        //  margin)), 0.0, 0.0);
-        painter.drawRoundedRect(
-          QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)),
-          0.0,
-          0.0);
+    // Right
+    QPointF right0(width - margin, height / 2);
+    QPointF right1(width, height / 2);
+    gradient.setStart(right0);
+    gradient.setFinalStop(right1);
+    painter.setBrush(QBrush(gradient));
+    // Deprecated in 5.13: painter.drawRoundRect(
+    //  QRectF(QPointF(width - margin * radius, margin), QPointF(width, height -
+    //  margin)), 0.0, 0.0);
+    painter.drawRoundedRect(
+      QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), 0.0, 0.0);
 
-        // Left
-        QPointF left0(margin, height / 2);
-        QPointF left1(0, height / 2);
-        gradient.setStart(left0);
-        gradient.setFinalStop(left1);
-        painter.setBrush(QBrush(gradient));
-        painter.drawRoundedRect(
-          QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0);
+    // Left
+    QPointF left0(margin, height / 2);
+    QPointF left1(0, height / 2);
+    gradient.setStart(left0);
+    gradient.setFinalStop(left1);
+    painter.setBrush(QBrush(gradient));
+    painter.drawRoundedRect(
+      QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0);
 
-        // Top
-        QPointF top0(width / 2, margin);
-        QPointF top1(width / 2, 0);
-        gradient.setStart(top0);
-        gradient.setFinalStop(top1);
-        painter.setBrush(QBrush(gradient));
-        painter.drawRoundedRect(
-          QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0);
+    // Top
+    QPointF top0(width / 2, margin);
+    QPointF top1(width / 2, 0);
+    gradient.setStart(top0);
+    gradient.setFinalStop(top1);
+    painter.setBrush(QBrush(gradient));
+    painter.drawRoundedRect(QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0);
 
-        // Bottom
-        QPointF bottom0(width / 2, height - margin);
-        QPointF bottom1(width / 2, height);
-        gradient.setStart(bottom0);
-        gradient.setFinalStop(bottom1);
-        painter.setBrush(QBrush(gradient));
-        painter.drawRoundedRect(
-          QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0);
+    // Bottom
+    QPointF bottom0(width / 2, height - margin);
+    QPointF bottom1(width / 2, height);
+    gradient.setStart(bottom0);
+    gradient.setFinalStop(bottom1);
+    painter.setBrush(QBrush(gradient));
+    painter.drawRoundedRect(
+      QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0);
 
-        // BottomRight
-        QPointF bottomright0(width - margin, height - margin);
-        QPointF bottomright1(width, height);
-        gradient.setStart(bottomright0);
-        gradient.setFinalStop(bottomright1);
-        gradient.setColorAt(endPosition1, end);
-        painter.setBrush(QBrush(gradient));
-        painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0);
+    // BottomRight
+    QPointF bottomright0(width - margin, height - margin);
+    QPointF bottomright1(width, height);
+    gradient.setStart(bottomright0);
+    gradient.setFinalStop(bottomright1);
+    gradient.setColorAt(endPosition1, end);
+    painter.setBrush(QBrush(gradient));
+    painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0);
 
-        // BottomLeft
-        QPointF bottomleft0(margin, height - margin);
-        QPointF bottomleft1(0, height);
-        gradient.setStart(bottomleft0);
-        gradient.setFinalStop(bottomleft1);
-        gradient.setColorAt(endPosition1, end);
-        painter.setBrush(QBrush(gradient));
-        painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0);
+    // BottomLeft
+    QPointF bottomleft0(margin, height - margin);
+    QPointF bottomleft1(0, height);
+    gradient.setStart(bottomleft0);
+    gradient.setFinalStop(bottomleft1);
+    gradient.setColorAt(endPosition1, end);
+    painter.setBrush(QBrush(gradient));
+    painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0);
 
-        // TopLeft
-        QPointF topleft0(margin, margin);
-        QPointF topleft1(0, 0);
-        gradient.setStart(topleft0);
-        gradient.setFinalStop(topleft1);
-        gradient.setColorAt(endPosition1, end);
-        painter.setBrush(QBrush(gradient));
-        painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0);
+    // TopLeft
+    QPointF topleft0(margin, margin);
+    QPointF topleft1(0, 0);
+    gradient.setStart(topleft0);
+    gradient.setFinalStop(topleft1);
+    gradient.setColorAt(endPosition1, end);
+    painter.setBrush(QBrush(gradient));
+    painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0);
 
-        // TopRight
-        QPointF topright0(width - margin, margin);
-        QPointF topright1(width, 0);
-        gradient.setStart(topright0);
-        gradient.setFinalStop(topright1);
-        gradient.setColorAt(endPosition1, end);
-        painter.setBrush(QBrush(gradient));
-        painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0);
+    // TopRight
+    QPointF topright0(width - margin, margin);
+    QPointF topright1(width, 0);
+    gradient.setStart(topright0);
+    gradient.setFinalStop(topright1);
+    gradient.setColorAt(endPosition1, end);
+    painter.setBrush(QBrush(gradient));
+    painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0);
 
-        // Widget
-        painter.setBrush(QBrush("#FFFFFF"));
-        painter.setRenderHint(QPainter::Antialiasing);
-        painter.drawRoundedRect(
-          QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)),
-          radius,
-          radius);
+    // Widget
+    painter.setBrush(QBrush("#FFFFFF"));
+    painter.setRenderHint(QPainter::Antialiasing);
+    painter.drawRoundedRect(
+      QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), radius, radius);
 }
diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h
index 4ace473148e378f43d5ddae45dde08d3083413ab..f089d0b3768dbe0fe838538447f0179284c58896 100644
--- a/src/ui/DropShadow.h
+++ b/src/ui/DropShadow.h
@@ -11,14 +11,14 @@ class QPainter;
 class DropShadow
 {
 public:
-        static void draw(QPainter &painter,
-                         qint16 margin,
-                         qreal radius,
-                         QColor start,
-                         QColor end,
-                         qreal startPosition,
-                         qreal endPosition0,
-                         qreal endPosition1,
-                         qreal width,
-                         qreal height);
+    static void draw(QPainter &painter,
+                     qint16 margin,
+                     qreal radius,
+                     QColor start,
+                     QColor end,
+                     qreal startPosition,
+                     qreal endPosition0,
+                     qreal endPosition1,
+                     qreal width,
+                     qreal height);
 };
diff --git a/src/ui/FlatButton.cpp b/src/ui/FlatButton.cpp
index c036401be991794a89b242343f176fa3607a1a2e..4d19c8bb19b549932e56871f3b283d05a948145f 100644
--- a/src/ui/FlatButton.cpp
+++ b/src/ui/FlatButton.cpp
@@ -30,60 +30,60 @@
 static QString
 removeKDEAccelerators(QString text)
 {
-        return text.remove(QChar('&'));
+    return text.remove(QChar('&'));
 }
 
 void
 FlatButton::init()
 {
-        ripple_overlay_          = new RippleOverlay(this);
-        state_machine_           = new FlatButtonStateMachine(this);
-        role_                    = ui::Role::Default;
-        ripple_style_            = ui::RippleStyle::PositionedRipple;
-        icon_placement_          = ui::ButtonIconPlacement::LeftIcon;
-        overlay_style_           = ui::OverlayStyle::GrayOverlay;
-        bg_mode_                 = Qt::TransparentMode;
-        fixed_ripple_radius_     = 64;
-        corner_radius_           = 3;
-        base_opacity_            = 0.13;
-        font_size_               = 10; // 10.5;
-        use_fixed_ripple_radius_ = false;
-
-        setStyle(&ThemeManager::instance());
-        setAttribute(Qt::WA_Hover);
-        setMouseTracking(true);
-        setCursor(QCursor(Qt::PointingHandCursor));
+    ripple_overlay_          = new RippleOverlay(this);
+    state_machine_           = new FlatButtonStateMachine(this);
+    role_                    = ui::Role::Default;
+    ripple_style_            = ui::RippleStyle::PositionedRipple;
+    icon_placement_          = ui::ButtonIconPlacement::LeftIcon;
+    overlay_style_           = ui::OverlayStyle::GrayOverlay;
+    bg_mode_                 = Qt::TransparentMode;
+    fixed_ripple_radius_     = 64;
+    corner_radius_           = 3;
+    base_opacity_            = 0.13;
+    font_size_               = 10; // 10.5;
+    use_fixed_ripple_radius_ = false;
 
-        QPainterPath path;
-        path.addRoundedRect(rect(), corner_radius_, corner_radius_);
+    setStyle(&ThemeManager::instance());
+    setAttribute(Qt::WA_Hover);
+    setMouseTracking(true);
+    setCursor(QCursor(Qt::PointingHandCursor));
+
+    QPainterPath path;
+    path.addRoundedRect(rect(), corner_radius_, corner_radius_);
 
-        ripple_overlay_->setClipPath(path);
-        ripple_overlay_->setClipping(true);
+    ripple_overlay_->setClipPath(path);
+    ripple_overlay_->setClipping(true);
 
-        state_machine_->setupProperties();
-        state_machine_->startAnimations();
+    state_machine_->setupProperties();
+    state_machine_->startAnimations();
 }
 
 FlatButton::FlatButton(QWidget *parent, ui::ButtonPreset preset)
   : QPushButton(parent)
 {
-        init();
-        applyPreset(preset);
+    init();
+    applyPreset(preset);
 }
 
 FlatButton::FlatButton(const QString &text, QWidget *parent, ui::ButtonPreset preset)
   : QPushButton(text, parent)
 {
-        init();
-        applyPreset(preset);
+    init();
+    applyPreset(preset);
 }
 
 FlatButton::FlatButton(const QString &text, ui::Role role, QWidget *parent, ui::ButtonPreset preset)
   : QPushButton(text, parent)
 {
-        init();
-        applyPreset(preset);
-        setRole(role);
+    init();
+    applyPreset(preset);
+    setRole(role);
 }
 
 FlatButton::~FlatButton() {}
@@ -91,406 +91,406 @@ FlatButton::~FlatButton() {}
 void
 FlatButton::applyPreset(ui::ButtonPreset preset)
 {
-        switch (preset) {
-        case ui::ButtonPreset::FlatPreset:
-                setOverlayStyle(ui::OverlayStyle::NoOverlay);
-                break;
-        case ui::ButtonPreset::CheckablePreset:
-                setOverlayStyle(ui::OverlayStyle::NoOverlay);
-                setCheckable(true);
-                break;
-        default:
-                break;
-        }
+    switch (preset) {
+    case ui::ButtonPreset::FlatPreset:
+        setOverlayStyle(ui::OverlayStyle::NoOverlay);
+        break;
+    case ui::ButtonPreset::CheckablePreset:
+        setOverlayStyle(ui::OverlayStyle::NoOverlay);
+        setCheckable(true);
+        break;
+    default:
+        break;
+    }
 }
 
 void
 FlatButton::setRole(ui::Role role)
 {
-        role_ = role;
-        state_machine_->setupProperties();
+    role_ = role;
+    state_machine_->setupProperties();
 }
 
 ui::Role
 FlatButton::role() const
 {
-        return role_;
+    return role_;
 }
 
 void
 FlatButton::setForegroundColor(const QColor &color)
 {
-        foreground_color_ = color;
+    foreground_color_ = color;
 }
 
 QColor
 FlatButton::foregroundColor() const
 {
-        if (!foreground_color_.isValid()) {
-                if (bg_mode_ == Qt::OpaqueMode) {
-                        return ThemeManager::instance().themeColor("BrightWhite");
-                }
-
-                switch (role_) {
-                case ui::Role::Primary:
-                        return ThemeManager::instance().themeColor("Blue");
-                case ui::Role::Secondary:
-                        return ThemeManager::instance().themeColor("Gray");
-                case ui::Role::Default:
-                default:
-                        return ThemeManager::instance().themeColor("Black");
-                }
+    if (!foreground_color_.isValid()) {
+        if (bg_mode_ == Qt::OpaqueMode) {
+            return ThemeManager::instance().themeColor("BrightWhite");
+        }
+
+        switch (role_) {
+        case ui::Role::Primary:
+            return ThemeManager::instance().themeColor("Blue");
+        case ui::Role::Secondary:
+            return ThemeManager::instance().themeColor("Gray");
+        case ui::Role::Default:
+        default:
+            return ThemeManager::instance().themeColor("Black");
         }
+    }
 
-        return foreground_color_;
+    return foreground_color_;
 }
 
 void
 FlatButton::setBackgroundColor(const QColor &color)
 {
-        background_color_ = color;
+    background_color_ = color;
 }
 
 QColor
 FlatButton::backgroundColor() const
 {
-        if (!background_color_.isValid()) {
-                switch (role_) {
-                case ui::Role::Primary:
-                        return ThemeManager::instance().themeColor("Blue");
-                case ui::Role::Secondary:
-                        return ThemeManager::instance().themeColor("Gray");
-                case ui::Role::Default:
-                default:
-                        return ThemeManager::instance().themeColor("Black");
-                }
+    if (!background_color_.isValid()) {
+        switch (role_) {
+        case ui::Role::Primary:
+            return ThemeManager::instance().themeColor("Blue");
+        case ui::Role::Secondary:
+            return ThemeManager::instance().themeColor("Gray");
+        case ui::Role::Default:
+        default:
+            return ThemeManager::instance().themeColor("Black");
         }
+    }
 
-        return background_color_;
+    return background_color_;
 }
 
 void
 FlatButton::setOverlayColor(const QColor &color)
 {
-        overlay_color_ = color;
-        setOverlayStyle(ui::OverlayStyle::TintedOverlay);
+    overlay_color_ = color;
+    setOverlayStyle(ui::OverlayStyle::TintedOverlay);
 }
 
 QColor
 FlatButton::overlayColor() const
 {
-        if (!overlay_color_.isValid()) {
-                return foregroundColor();
-        }
+    if (!overlay_color_.isValid()) {
+        return foregroundColor();
+    }
 
-        return overlay_color_;
+    return overlay_color_;
 }
 
 void
 FlatButton::setDisabledForegroundColor(const QColor &color)
 {
-        disabled_color_ = color;
+    disabled_color_ = color;
 }
 
 QColor
 FlatButton::disabledForegroundColor() const
 {
-        if (!disabled_color_.isValid()) {
-                return ThemeManager::instance().themeColor("FadedWhite");
-        }
+    if (!disabled_color_.isValid()) {
+        return ThemeManager::instance().themeColor("FadedWhite");
+    }
 
-        return disabled_color_;
+    return disabled_color_;
 }
 
 void
 FlatButton::setDisabledBackgroundColor(const QColor &color)
 {
-        disabled_background_color_ = color;
+    disabled_background_color_ = color;
 }
 
 QColor
 FlatButton::disabledBackgroundColor() const
 {
-        if (!disabled_background_color_.isValid()) {
-                return ThemeManager::instance().themeColor("FadedWhite");
-        }
+    if (!disabled_background_color_.isValid()) {
+        return ThemeManager::instance().themeColor("FadedWhite");
+    }
 
-        return disabled_background_color_;
+    return disabled_background_color_;
 }
 
 void
 FlatButton::setFontSize(qreal size)
 {
-        font_size_ = size;
+    font_size_ = size;
 
-        QFont f(font());
-        f.setPointSizeF(size);
-        setFont(f);
+    QFont f(font());
+    f.setPointSizeF(size);
+    setFont(f);
 
-        update();
+    update();
 }
 
 qreal
 FlatButton::fontSize() const
 {
-        return font_size_;
+    return font_size_;
 }
 
 void
 FlatButton::setOverlayStyle(ui::OverlayStyle style)
 {
-        overlay_style_ = style;
-        update();
+    overlay_style_ = style;
+    update();
 }
 
 ui::OverlayStyle
 FlatButton::overlayStyle() const
 {
-        return overlay_style_;
+    return overlay_style_;
 }
 
 void
 FlatButton::setRippleStyle(ui::RippleStyle style)
 {
-        ripple_style_ = style;
+    ripple_style_ = style;
 }
 
 ui::RippleStyle
 FlatButton::rippleStyle() const
 {
-        return ripple_style_;
+    return ripple_style_;
 }
 
 void
 FlatButton::setIconPlacement(ui::ButtonIconPlacement placement)
 {
-        icon_placement_ = placement;
-        update();
+    icon_placement_ = placement;
+    update();
 }
 
 ui::ButtonIconPlacement
 FlatButton::iconPlacement() const
 {
-        return icon_placement_;
+    return icon_placement_;
 }
 
 void
 FlatButton::setCornerRadius(qreal radius)
 {
-        corner_radius_ = radius;
-        updateClipPath();
-        update();
+    corner_radius_ = radius;
+    updateClipPath();
+    update();
 }
 
 qreal
 FlatButton::cornerRadius() const
 {
-        return corner_radius_;
+    return corner_radius_;
 }
 
 void
 FlatButton::setBackgroundMode(Qt::BGMode mode)
 {
-        bg_mode_ = mode;
-        state_machine_->setupProperties();
+    bg_mode_ = mode;
+    state_machine_->setupProperties();
 }
 
 Qt::BGMode
 FlatButton::backgroundMode() const
 {
-        return bg_mode_;
+    return bg_mode_;
 }
 
 void
 FlatButton::setBaseOpacity(qreal opacity)
 {
-        base_opacity_ = opacity;
-        state_machine_->setupProperties();
+    base_opacity_ = opacity;
+    state_machine_->setupProperties();
 }
 
 qreal
 FlatButton::baseOpacity() const
 {
-        return base_opacity_;
+    return base_opacity_;
 }
 
 void
 FlatButton::setCheckable(bool value)
 {
-        state_machine_->updateCheckedStatus();
-        state_machine_->setCheckedOverlayProgress(0);
+    state_machine_->updateCheckedStatus();
+    state_machine_->setCheckedOverlayProgress(0);
 
-        QPushButton::setCheckable(value);
+    QPushButton::setCheckable(value);
 }
 
 void
 FlatButton::setHasFixedRippleRadius(bool value)
 {
-        use_fixed_ripple_radius_ = value;
+    use_fixed_ripple_radius_ = value;
 }
 
 bool
 FlatButton::hasFixedRippleRadius() const
 {
-        return use_fixed_ripple_radius_;
+    return use_fixed_ripple_radius_;
 }
 
 void
 FlatButton::setFixedRippleRadius(qreal radius)
 {
-        fixed_ripple_radius_ = radius;
-        setHasFixedRippleRadius(true);
+    fixed_ripple_radius_ = radius;
+    setHasFixedRippleRadius(true);
 }
 
 QSize
 FlatButton::sizeHint() const
 {
-        ensurePolished();
+    ensurePolished();
 
-        QSize label(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
+    QSize label(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
 
-        int w = 20 + label.width();
-        int h = label.height();
+    int w = 20 + label.width();
+    int h = label.height();
 
-        if (!icon().isNull()) {
-                w += iconSize().width() + FlatButton::IconPadding;
-                h = qMax(h, iconSize().height());
-        }
+    if (!icon().isNull()) {
+        w += iconSize().width() + FlatButton::IconPadding;
+        h = qMax(h, iconSize().height());
+    }
 
-        return QSize(w, 20 + h);
+    return QSize(w, 20 + h);
 }
 
 void
 FlatButton::checkStateSet()
 {
-        state_machine_->updateCheckedStatus();
-        QPushButton::checkStateSet();
+    state_machine_->updateCheckedStatus();
+    QPushButton::checkStateSet();
 }
 
 void
 FlatButton::mousePressEvent(QMouseEvent *event)
 {
-        if (ui::RippleStyle::NoRipple != ripple_style_) {
-                QPoint pos;
-                qreal radiusEndValue;
+    if (ui::RippleStyle::NoRipple != ripple_style_) {
+        QPoint pos;
+        qreal radiusEndValue;
 
-                if (ui::RippleStyle::CenteredRipple == ripple_style_) {
-                        pos = rect().center();
-                } else {
-                        pos = event->pos();
-                }
+        if (ui::RippleStyle::CenteredRipple == ripple_style_) {
+            pos = rect().center();
+        } else {
+            pos = event->pos();
+        }
 
-                if (use_fixed_ripple_radius_) {
-                        radiusEndValue = fixed_ripple_radius_;
-                } else {
-                        radiusEndValue = static_cast<qreal>(width()) / 2;
-                }
+        if (use_fixed_ripple_radius_) {
+            radiusEndValue = fixed_ripple_radius_;
+        } else {
+            radiusEndValue = static_cast<qreal>(width()) / 2;
+        }
 
-                Ripple *ripple = new Ripple(pos);
+        Ripple *ripple = new Ripple(pos);
 
-                ripple->setRadiusEndValue(radiusEndValue);
-                ripple->setOpacityStartValue(0.35);
-                ripple->setColor(foregroundColor());
-                ripple->radiusAnimation()->setDuration(250);
-                ripple->opacityAnimation()->setDuration(250);
+        ripple->setRadiusEndValue(radiusEndValue);
+        ripple->setOpacityStartValue(0.35);
+        ripple->setColor(foregroundColor());
+        ripple->radiusAnimation()->setDuration(250);
+        ripple->opacityAnimation()->setDuration(250);
 
-                ripple_overlay_->addRipple(ripple);
-        }
+        ripple_overlay_->addRipple(ripple);
+    }
 
-        QPushButton::mousePressEvent(event);
+    QPushButton::mousePressEvent(event);
 }
 
 void
 FlatButton::mouseReleaseEvent(QMouseEvent *event)
 {
-        QPushButton::mouseReleaseEvent(event);
-        state_machine_->updateCheckedStatus();
+    QPushButton::mouseReleaseEvent(event);
+    state_machine_->updateCheckedStatus();
 }
 
 void
 FlatButton::resizeEvent(QResizeEvent *event)
 {
-        QPushButton::resizeEvent(event);
-        updateClipPath();
+    QPushButton::resizeEvent(event);
+    updateClipPath();
 }
 
 void
 FlatButton::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event)
+    Q_UNUSED(event)
 
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
 
-        const qreal cr = corner_radius_;
+    const qreal cr = corner_radius_;
 
-        if (cr > 0) {
-                QPainterPath path;
-                path.addRoundedRect(rect(), cr, cr);
+    if (cr > 0) {
+        QPainterPath path;
+        path.addRoundedRect(rect(), cr, cr);
 
-                painter.setClipPath(path);
-                painter.setClipping(true);
-        }
+        painter.setClipPath(path);
+        painter.setClipping(true);
+    }
 
-        paintBackground(&painter);
+    paintBackground(&painter);
 
-        painter.setOpacity(1);
-        painter.setClipping(false);
+    painter.setOpacity(1);
+    painter.setClipping(false);
 
-        paintForeground(&painter);
+    paintForeground(&painter);
 }
 
 void
 FlatButton::paintBackground(QPainter *painter)
 {
-        const qreal overlayOpacity  = state_machine_->overlayOpacity();
-        const qreal checkedProgress = state_machine_->checkedOverlayProgress();
+    const qreal overlayOpacity  = state_machine_->overlayOpacity();
+    const qreal checkedProgress = state_machine_->checkedOverlayProgress();
 
-        if (Qt::OpaqueMode == bg_mode_) {
-                QBrush brush;
-                brush.setStyle(Qt::SolidPattern);
-
-                if (isEnabled()) {
-                        brush.setColor(backgroundColor());
-                } else {
-                        brush.setColor(disabledBackgroundColor());
-                }
+    if (Qt::OpaqueMode == bg_mode_) {
+        QBrush brush;
+        brush.setStyle(Qt::SolidPattern);
 
-                painter->setOpacity(1);
-                painter->setBrush(brush);
-                painter->setPen(Qt::NoPen);
-                painter->drawRect(rect());
+        if (isEnabled()) {
+            brush.setColor(backgroundColor());
+        } else {
+            brush.setColor(disabledBackgroundColor());
         }
 
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
+        painter->setOpacity(1);
+        painter->setBrush(brush);
         painter->setPen(Qt::NoPen);
+        painter->drawRect(rect());
+    }
 
-        if (!isEnabled()) {
-                return;
-        }
+    QBrush brush;
+    brush.setStyle(Qt::SolidPattern);
+    painter->setPen(Qt::NoPen);
 
-        if ((ui::OverlayStyle::NoOverlay != overlay_style_) && (overlayOpacity > 0)) {
-                if (ui::OverlayStyle::TintedOverlay == overlay_style_) {
-                        brush.setColor(overlayColor());
-                } else {
-                        brush.setColor(Qt::gray);
-                }
+    if (!isEnabled()) {
+        return;
+    }
 
-                painter->setOpacity(overlayOpacity);
-                painter->setBrush(brush);
-                painter->drawRect(rect());
+    if ((ui::OverlayStyle::NoOverlay != overlay_style_) && (overlayOpacity > 0)) {
+        if (ui::OverlayStyle::TintedOverlay == overlay_style_) {
+            brush.setColor(overlayColor());
+        } else {
+            brush.setColor(Qt::gray);
         }
 
-        if (isCheckable() && checkedProgress > 0) {
-                const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7;
-                brush.setColor(foregroundColor());
-                painter->setOpacity(q * checkedProgress);
-                painter->setBrush(brush);
-                QRect r(rect());
-                r.setHeight(static_cast<qreal>(r.height()) * checkedProgress);
-                painter->drawRect(r);
-        }
+        painter->setOpacity(overlayOpacity);
+        painter->setBrush(brush);
+        painter->drawRect(rect());
+    }
+
+    if (isCheckable() && checkedProgress > 0) {
+        const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7;
+        brush.setColor(foregroundColor());
+        painter->setOpacity(q * checkedProgress);
+        painter->setBrush(brush);
+        QRect r(rect());
+        r.setHeight(static_cast<qreal>(r.height()) * checkedProgress);
+        painter->drawRect(r);
+    }
 }
 
 #define COLOR_INTERPOLATE(CH) (1 - progress) * source.CH() + progress *dest.CH()
@@ -498,64 +498,63 @@ FlatButton::paintBackground(QPainter *painter)
 void
 FlatButton::paintForeground(QPainter *painter)
 {
-        if (isEnabled()) {
-                painter->setPen(foregroundColor());
-                const qreal progress = state_machine_->checkedOverlayProgress();
-
-                if (isCheckable() && progress > 0) {
-                        QColor source = foregroundColor();
-                        QColor dest =
-                          Qt::TransparentMode == bg_mode_ ? Qt::white : backgroundColor();
-                        if (qFuzzyCompare(1, progress)) {
-                                painter->setPen(dest);
-                        } else {
-                                painter->setPen(QColor(COLOR_INTERPOLATE(red),
-                                                       COLOR_INTERPOLATE(green),
-                                                       COLOR_INTERPOLATE(blue),
-                                                       COLOR_INTERPOLATE(alpha)));
-                        }
-                }
-        } else {
-                painter->setPen(disabledForegroundColor());
+    if (isEnabled()) {
+        painter->setPen(foregroundColor());
+        const qreal progress = state_machine_->checkedOverlayProgress();
+
+        if (isCheckable() && progress > 0) {
+            QColor source = foregroundColor();
+            QColor dest   = Qt::TransparentMode == bg_mode_ ? Qt::white : backgroundColor();
+            if (qFuzzyCompare(1, progress)) {
+                painter->setPen(dest);
+            } else {
+                painter->setPen(QColor(COLOR_INTERPOLATE(red),
+                                       COLOR_INTERPOLATE(green),
+                                       COLOR_INTERPOLATE(blue),
+                                       COLOR_INTERPOLATE(alpha)));
+            }
         }
+    } else {
+        painter->setPen(disabledForegroundColor());
+    }
 
-        if (icon().isNull()) {
-                painter->drawText(rect(), Qt::AlignCenter, removeKDEAccelerators(text()));
-                return;
-        }
+    if (icon().isNull()) {
+        painter->drawText(rect(), Qt::AlignCenter, removeKDEAccelerators(text()));
+        return;
+    }
 
-        QSize textSize(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
-        QSize base(size() - textSize);
+    QSize textSize(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
+    QSize base(size() - textSize);
 
-        const int iw = iconSize().width() + IconPadding;
-        QPoint pos((base.width() - iw) / 2, 0);
+    const int iw = iconSize().width() + IconPadding;
+    QPoint pos((base.width() - iw) / 2, 0);
 
-        QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize);
-        QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize());
+    QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize);
+    QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize());
 
-        /* if (ui::LeftIcon == icon_placement_) { */
-        /* 	textGeometry.translate(iw, 0); */
-        /* } else { */
-        /* 	iconGeometry.translate(textSize.width() + IconPadding, 0); */
-        /* } */
+    /* if (ui::LeftIcon == icon_placement_) { */
+    /* 	textGeometry.translate(iw, 0); */
+    /* } else { */
+    /* 	iconGeometry.translate(textSize.width() + IconPadding, 0); */
+    /* } */
 
-        painter->drawText(textGeometry, Qt::AlignCenter, removeKDEAccelerators(text()));
+    painter->drawText(textGeometry, Qt::AlignCenter, removeKDEAccelerators(text()));
 
-        QPixmap pixmap = icon().pixmap(iconSize());
-        QPainter icon(&pixmap);
-        icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
-        icon.fillRect(pixmap.rect(), painter->pen().color());
-        painter->drawPixmap(iconGeometry, pixmap);
+    QPixmap pixmap = icon().pixmap(iconSize());
+    QPainter icon(&pixmap);
+    icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
+    icon.fillRect(pixmap.rect(), painter->pen().color());
+    painter->drawPixmap(iconGeometry, pixmap);
 }
 
 void
 FlatButton::updateClipPath()
 {
-        const qreal radius = corner_radius_;
+    const qreal radius = corner_radius_;
 
-        QPainterPath path;
-        path.addRoundedRect(rect(), radius, radius);
-        ripple_overlay_->setClipPath(path);
+    QPainterPath path;
+    path.addRoundedRect(rect(), radius, radius);
+    ripple_overlay_->setClipPath(path);
 }
 
 FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent)
@@ -575,45 +574,45 @@ FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent)
   , checked_overlay_progress_(parent->isChecked() ? 1 : 0)
   , was_checked_(false)
 {
-        Q_ASSERT(parent);
+    Q_ASSERT(parent);
 
-        parent->installEventFilter(this);
+    parent->installEventFilter(this);
 
-        config_state_->setInitialState(neutral_state_);
-        addState(top_level_state_);
-        setInitialState(top_level_state_);
+    config_state_->setInitialState(neutral_state_);
+    addState(top_level_state_);
+    setInitialState(top_level_state_);
 
-        checkable_state_->setInitialState(parent->isChecked() ? checked_state_ : unchecked_state_);
-        QSignalTransition *transition;
-        QPropertyAnimation *animation;
+    checkable_state_->setInitialState(parent->isChecked() ? checked_state_ : unchecked_state_);
+    QSignalTransition *transition;
+    QPropertyAnimation *animation;
 
-        transition = new QSignalTransition(this, SIGNAL(buttonChecked()));
-        transition->setTargetState(checked_state_);
-        unchecked_state_->addTransition(transition);
+    transition = new QSignalTransition(this, SIGNAL(buttonChecked()));
+    transition->setTargetState(checked_state_);
+    unchecked_state_->addTransition(transition);
 
-        animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
-        animation->setDuration(200);
-        transition->addAnimation(animation);
+    animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
+    animation->setDuration(200);
+    transition->addAnimation(animation);
 
-        transition = new QSignalTransition(this, SIGNAL(buttonUnchecked()));
-        transition->setTargetState(unchecked_state_);
-        checked_state_->addTransition(transition);
+    transition = new QSignalTransition(this, SIGNAL(buttonUnchecked()));
+    transition->setTargetState(unchecked_state_);
+    checked_state_->addTransition(transition);
 
-        animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
-        animation->setDuration(200);
-        transition->addAnimation(animation);
+    animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
+    animation->setDuration(200);
+    transition->addAnimation(animation);
 
-        addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_);
-        addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_);
-        addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_);
-        addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_);
-        addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_);
-        addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_);
-        addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_);
-        addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_);
-        addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_);
-        addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_);
-        addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_);
+    addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_);
+    addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_);
+    addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_);
+    addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_);
+    addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_);
+    addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_);
+    addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_);
+    addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_);
+    addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_);
+    addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_);
+    addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_);
 }
 
 FlatButtonStateMachine::~FlatButtonStateMachine() {}
@@ -621,73 +620,73 @@ FlatButtonStateMachine::~FlatButtonStateMachine() {}
 void
 FlatButtonStateMachine::setOverlayOpacity(qreal opacity)
 {
-        overlay_opacity_ = opacity;
-        button_->update();
+    overlay_opacity_ = opacity;
+    button_->update();
 }
 
 void
 FlatButtonStateMachine::setCheckedOverlayProgress(qreal opacity)
 {
-        checked_overlay_progress_ = opacity;
-        button_->update();
+    checked_overlay_progress_ = opacity;
+    button_->update();
 }
 
 void
 FlatButtonStateMachine::startAnimations()
 {
-        start();
+    start();
 }
 
 void
 FlatButtonStateMachine::setupProperties()
 {
-        QColor overlayColor;
+    QColor overlayColor;
 
-        if (Qt::TransparentMode == button_->backgroundMode()) {
-                overlayColor = button_->backgroundColor();
-        } else {
-                overlayColor = button_->foregroundColor();
-        }
+    if (Qt::TransparentMode == button_->backgroundMode()) {
+        overlayColor = button_->backgroundColor();
+    } else {
+        overlayColor = button_->foregroundColor();
+    }
 
-        const qreal baseOpacity = button_->baseOpacity();
+    const qreal baseOpacity = button_->baseOpacity();
 
-        neutral_state_->assignProperty(this, "overlayOpacity", 0);
-        neutral_focused_state_->assignProperty(this, "overlayOpacity", 0);
-        hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity);
-        hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity);
-        pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity);
-        checked_state_->assignProperty(this, "checkedOverlayProgress", 1);
-        unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0);
+    neutral_state_->assignProperty(this, "overlayOpacity", 0);
+    neutral_focused_state_->assignProperty(this, "overlayOpacity", 0);
+    hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity);
+    hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity);
+    pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity);
+    checked_state_->assignProperty(this, "checkedOverlayProgress", 1);
+    unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0);
 
-        button_->update();
+    button_->update();
 }
 
 void
 FlatButtonStateMachine::updateCheckedStatus()
 {
-        const bool checked = button_->isChecked();
-        if (was_checked_ != checked) {
-                was_checked_ = checked;
-                if (checked) {
-                        emit buttonChecked();
-                } else {
-                        emit buttonUnchecked();
-                }
+    const bool checked = button_->isChecked();
+    if (was_checked_ != checked) {
+        was_checked_ = checked;
+        if (checked) {
+            emit buttonChecked();
+        } else {
+            emit buttonUnchecked();
         }
+    }
 }
 
 bool
 FlatButtonStateMachine::eventFilter(QObject *watched, QEvent *event)
 {
-        if (QEvent::FocusIn == event->type()) {
-                QFocusEvent *focusEvent = static_cast<QFocusEvent *>(event);
-                if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) {
-                        emit buttonPressed();
-                        return true;
-                }
+    if (QEvent::FocusIn == event->type()) {
+        QFocusEvent *focusEvent = static_cast<QFocusEvent *>(event);
+        if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) {
+            emit buttonPressed();
+            return true;
         }
+    }
 
-        return QStateMachine::eventFilter(watched, event);
+    return QStateMachine::eventFilter(watched, event);
 }
 
 void
@@ -696,7 +695,7 @@ FlatButtonStateMachine::addTransition(QObject *object,
                                       QState *fromState,
                                       QState *toState)
 {
-        addTransition(new QSignalTransition(object, signal), fromState, toState);
+    addTransition(new QSignalTransition(object, signal), fromState, toState);
 }
 
 void
@@ -705,7 +704,7 @@ FlatButtonStateMachine::addTransition(QObject *object,
                                       QState *fromState,
                                       QState *toState)
 {
-        addTransition(new QEventTransition(object, eventType), fromState, toState);
+    addTransition(new QEventTransition(object, eventType), fromState, toState);
 }
 
 void
@@ -713,13 +712,13 @@ FlatButtonStateMachine::addTransition(QAbstractTransition *transition,
                                       QState *fromState,
                                       QState *toState)
 {
-        transition->setTargetState(toState);
+    transition->setTargetState(toState);
 
-        QPropertyAnimation *animation;
+    QPropertyAnimation *animation;
 
-        animation = new QPropertyAnimation(this, "overlayOpacity", this);
-        animation->setDuration(150);
-        transition->addAnimation(animation);
+    animation = new QPropertyAnimation(this, "overlayOpacity", this);
+    animation->setDuration(150);
+    transition->addAnimation(animation);
 
-        fromState->addTransition(transition);
+    fromState->addTransition(transition);
 }
diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h
index c79945b7f45e01b6b203a8b5cb91edc01dfbc467..b39c94ac4fc2d4c6c44eb45f0929e94b83684977 100644
--- a/src/ui/FlatButton.h
+++ b/src/ui/FlatButton.h
@@ -14,174 +14,171 @@ class FlatButton;
 
 class FlatButtonStateMachine : public QStateMachine
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity)
-        Q_PROPERTY(
-          qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ checkedOverlayProgress)
+    Q_PROPERTY(qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity)
+    Q_PROPERTY(
+      qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ checkedOverlayProgress)
 
 public:
-        explicit FlatButtonStateMachine(FlatButton *parent);
-        ~FlatButtonStateMachine() override;
+    explicit FlatButtonStateMachine(FlatButton *parent);
+    ~FlatButtonStateMachine() override;
 
-        void setOverlayOpacity(qreal opacity);
-        void setCheckedOverlayProgress(qreal opacity);
+    void setOverlayOpacity(qreal opacity);
+    void setCheckedOverlayProgress(qreal opacity);
 
-        inline qreal overlayOpacity() const;
-        inline qreal checkedOverlayProgress() const;
+    inline qreal overlayOpacity() const;
+    inline qreal checkedOverlayProgress() const;
 
-        void startAnimations();
-        void setupProperties();
-        void updateCheckedStatus();
+    void startAnimations();
+    void setupProperties();
+    void updateCheckedStatus();
 
 signals:
-        void buttonPressed();
-        void buttonChecked();
-        void buttonUnchecked();
+    void buttonPressed();
+    void buttonChecked();
+    void buttonUnchecked();
 
 protected:
-        bool eventFilter(QObject *watched, QEvent *event) override;
+    bool eventFilter(QObject *watched, QEvent *event) override;
 
 private:
-        void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState);
-        void addTransition(QObject *object,
-                           QEvent::Type eventType,
-                           QState *fromState,
-                           QState *toState);
-        void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState);
-
-        FlatButton *const button_;
-
-        QState *const top_level_state_;
-        QState *const config_state_;
-        QState *const checkable_state_;
-        QState *const checked_state_;
-        QState *const unchecked_state_;
-        QState *const neutral_state_;
-        QState *const neutral_focused_state_;
-        QState *const hovered_state_;
-        QState *const hovered_focused_state_;
-        QState *const pressed_state_;
-
-        qreal overlay_opacity_;
-        qreal checked_overlay_progress_;
-
-        bool was_checked_;
+    void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState);
+    void addTransition(QObject *object, QEvent::Type eventType, QState *fromState, QState *toState);
+    void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState);
+
+    FlatButton *const button_;
+
+    QState *const top_level_state_;
+    QState *const config_state_;
+    QState *const checkable_state_;
+    QState *const checked_state_;
+    QState *const unchecked_state_;
+    QState *const neutral_state_;
+    QState *const neutral_focused_state_;
+    QState *const hovered_state_;
+    QState *const hovered_focused_state_;
+    QState *const pressed_state_;
+
+    qreal overlay_opacity_;
+    qreal checked_overlay_progress_;
+
+    bool was_checked_;
 };
 
 inline qreal
 FlatButtonStateMachine::overlayOpacity() const
 {
-        return overlay_opacity_;
+    return overlay_opacity_;
 }
 
 inline qreal
 FlatButtonStateMachine::checkedOverlayProgress() const
 {
-        return checked_overlay_progress_;
+    return checked_overlay_progress_;
 }
 
 class FlatButton : public QPushButton
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-        Q_PROPERTY(QColor overlayColor WRITE setOverlayColor READ overlayColor)
-        Q_PROPERTY(QColor disabledForegroundColor WRITE setDisabledForegroundColor READ
-                     disabledForegroundColor)
-        Q_PROPERTY(QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ
-                     disabledBackgroundColor)
-        Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize)
+    Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor)
+    Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
+    Q_PROPERTY(QColor overlayColor WRITE setOverlayColor READ overlayColor)
+    Q_PROPERTY(
+      QColor disabledForegroundColor WRITE setDisabledForegroundColor READ disabledForegroundColor)
+    Q_PROPERTY(
+      QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ disabledBackgroundColor)
+    Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize)
 
 public:
-        explicit FlatButton(QWidget *parent         = nullptr,
-                            ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
-        explicit FlatButton(const QString &text,
-                            QWidget *parent         = nullptr,
-                            ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
-        FlatButton(const QString &text,
-                   ui::Role role,
-                   QWidget *parent         = nullptr,
-                   ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
-        ~FlatButton() override;
-
-        void applyPreset(ui::ButtonPreset preset);
-
-        void setBackgroundColor(const QColor &color);
-        void setBackgroundMode(Qt::BGMode mode);
-        void setBaseOpacity(qreal opacity);
-        void setCheckable(bool value);
-        void setCornerRadius(qreal radius);
-        void setDisabledBackgroundColor(const QColor &color);
-        void setDisabledForegroundColor(const QColor &color);
-        void setFixedRippleRadius(qreal radius);
-        void setFontSize(qreal size);
-        void setForegroundColor(const QColor &color);
-        void setHasFixedRippleRadius(bool value);
-        void setIconPlacement(ui::ButtonIconPlacement placement);
-        void setOverlayColor(const QColor &color);
-        void setOverlayStyle(ui::OverlayStyle style);
-        void setRippleStyle(ui::RippleStyle style);
-        void setRole(ui::Role role);
-
-        QColor foregroundColor() const;
-        QColor backgroundColor() const;
-        QColor overlayColor() const;
-        QColor disabledForegroundColor() const;
-        QColor disabledBackgroundColor() const;
-
-        qreal fontSize() const;
-        qreal cornerRadius() const;
-        qreal baseOpacity() const;
-
-        bool hasFixedRippleRadius() const;
-
-        ui::Role role() const;
-        ui::OverlayStyle overlayStyle() const;
-        ui::RippleStyle rippleStyle() const;
-        ui::ButtonIconPlacement iconPlacement() const;
-
-        Qt::BGMode backgroundMode() const;
-
-        QSize sizeHint() const override;
+    explicit FlatButton(QWidget *parent         = nullptr,
+                        ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
+    explicit FlatButton(const QString &text,
+                        QWidget *parent         = nullptr,
+                        ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
+    FlatButton(const QString &text,
+               ui::Role role,
+               QWidget *parent         = nullptr,
+               ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
+    ~FlatButton() override;
+
+    void applyPreset(ui::ButtonPreset preset);
+
+    void setBackgroundColor(const QColor &color);
+    void setBackgroundMode(Qt::BGMode mode);
+    void setBaseOpacity(qreal opacity);
+    void setCheckable(bool value);
+    void setCornerRadius(qreal radius);
+    void setDisabledBackgroundColor(const QColor &color);
+    void setDisabledForegroundColor(const QColor &color);
+    void setFixedRippleRadius(qreal radius);
+    void setFontSize(qreal size);
+    void setForegroundColor(const QColor &color);
+    void setHasFixedRippleRadius(bool value);
+    void setIconPlacement(ui::ButtonIconPlacement placement);
+    void setOverlayColor(const QColor &color);
+    void setOverlayStyle(ui::OverlayStyle style);
+    void setRippleStyle(ui::RippleStyle style);
+    void setRole(ui::Role role);
+
+    QColor foregroundColor() const;
+    QColor backgroundColor() const;
+    QColor overlayColor() const;
+    QColor disabledForegroundColor() const;
+    QColor disabledBackgroundColor() const;
+
+    qreal fontSize() const;
+    qreal cornerRadius() const;
+    qreal baseOpacity() const;
+
+    bool hasFixedRippleRadius() const;
+
+    ui::Role role() const;
+    ui::OverlayStyle overlayStyle() const;
+    ui::RippleStyle rippleStyle() const;
+    ui::ButtonIconPlacement iconPlacement() const;
+
+    Qt::BGMode backgroundMode() const;
+
+    QSize sizeHint() const override;
 
 protected:
-        int IconPadding = 0;
+    int IconPadding = 0;
 
-        void checkStateSet() override;
-        void mousePressEvent(QMouseEvent *event) override;
-        void mouseReleaseEvent(QMouseEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
+    void checkStateSet() override;
+    void mousePressEvent(QMouseEvent *event) override;
+    void mouseReleaseEvent(QMouseEvent *event) override;
+    void resizeEvent(QResizeEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
-        virtual void paintBackground(QPainter *painter);
-        virtual void paintForeground(QPainter *painter);
-        virtual void updateClipPath();
+    virtual void paintBackground(QPainter *painter);
+    virtual void paintForeground(QPainter *painter);
+    virtual void updateClipPath();
 
-        void init();
+    void init();
 
 private:
-        RippleOverlay *ripple_overlay_;
-        FlatButtonStateMachine *state_machine_;
+    RippleOverlay *ripple_overlay_;
+    FlatButtonStateMachine *state_machine_;
 
-        ui::Role role_;
-        ui::RippleStyle ripple_style_;
-        ui::ButtonIconPlacement icon_placement_;
-        ui::OverlayStyle overlay_style_;
+    ui::Role role_;
+    ui::RippleStyle ripple_style_;
+    ui::ButtonIconPlacement icon_placement_;
+    ui::OverlayStyle overlay_style_;
 
-        Qt::BGMode bg_mode_;
+    Qt::BGMode bg_mode_;
 
-        QColor background_color_;
-        QColor foreground_color_;
-        QColor overlay_color_;
-        QColor disabled_color_;
-        QColor disabled_background_color_;
+    QColor background_color_;
+    QColor foreground_color_;
+    QColor overlay_color_;
+    QColor disabled_color_;
+    QColor disabled_background_color_;
 
-        qreal fixed_ripple_radius_;
-        qreal corner_radius_;
-        qreal base_opacity_;
-        qreal font_size_;
+    qreal fixed_ripple_radius_;
+    qreal corner_radius_;
+    qreal base_opacity_;
+    qreal font_size_;
 
-        bool use_fixed_ripple_radius_;
+    bool use_fixed_ripple_radius_;
 };
diff --git a/src/ui/FloatingButton.cpp b/src/ui/FloatingButton.cpp
index 95b6ae1dc37f0ef84786fc5335b619d1f71f40d4..3f88e313dcef5a8bb70b77eef24e5916c843d258 100644
--- a/src/ui/FloatingButton.cpp
+++ b/src/ui/FloatingButton.cpp
@@ -10,91 +10,91 @@
 FloatingButton::FloatingButton(const QIcon &icon, QWidget *parent)
   : RaisedButton(parent)
 {
-        setFixedSize(DIAMETER, DIAMETER);
-        setGeometry(buttonGeometry());
+    setFixedSize(DIAMETER, DIAMETER);
+    setGeometry(buttonGeometry());
 
-        if (parentWidget())
-                parentWidget()->installEventFilter(this);
+    if (parentWidget())
+        parentWidget()->installEventFilter(this);
 
-        setFixedRippleRadius(50);
-        setIcon(icon);
-        raise();
+    setFixedRippleRadius(50);
+    setIcon(icon);
+    raise();
 }
 
 QRect
 FloatingButton::buttonGeometry() const
 {
-        QWidget *parent = parentWidget();
+    QWidget *parent = parentWidget();
 
-        if (!parent)
-                return QRect();
+    if (!parent)
+        return QRect();
 
-        return QRect(parent->width() - (OFFSET_X + DIAMETER),
-                     parent->height() - (OFFSET_Y + DIAMETER),
-                     DIAMETER,
-                     DIAMETER);
+    return QRect(parent->width() - (OFFSET_X + DIAMETER),
+                 parent->height() - (OFFSET_Y + DIAMETER),
+                 DIAMETER,
+                 DIAMETER);
 }
 
 bool
 FloatingButton::event(QEvent *event)
 {
-        if (!parent())
-                return RaisedButton::event(event);
-
-        switch (event->type()) {
-        case QEvent::ParentChange: {
-                parent()->installEventFilter(this);
-                setGeometry(buttonGeometry());
-                break;
-        }
-        case QEvent::ParentAboutToChange: {
-                parent()->installEventFilter(this);
-                break;
-        }
-        default:
-                break;
-        }
-
+    if (!parent())
         return RaisedButton::event(event);
+
+    switch (event->type()) {
+    case QEvent::ParentChange: {
+        parent()->installEventFilter(this);
+        setGeometry(buttonGeometry());
+        break;
+    }
+    case QEvent::ParentAboutToChange: {
+        parent()->installEventFilter(this);
+        break;
+    }
+    default:
+        break;
+    }
+
+    return RaisedButton::event(event);
 }
 
 bool
 FloatingButton::eventFilter(QObject *obj, QEvent *event)
 {
-        const QEvent::Type type = event->type();
+    const QEvent::Type type = event->type();
 
-        if (QEvent::Move == type || QEvent::Resize == type)
-                setGeometry(buttonGeometry());
+    if (QEvent::Move == type || QEvent::Resize == type)
+        setGeometry(buttonGeometry());
 
-        return RaisedButton::eventFilter(obj, event);
+    return RaisedButton::eventFilter(obj, event);
 }
 
 void
 FloatingButton::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event);
+    Q_UNUSED(event);
 
-        QRect square = QRect(0, 0, DIAMETER, DIAMETER);
-        square.moveCenter(rect().center());
+    QRect square = QRect(0, 0, DIAMETER, DIAMETER);
+    square.moveCenter(rect().center());
 
-        QPainter p(this);
-        p.setRenderHints(QPainter::Antialiasing);
+    QPainter p(this);
+    p.setRenderHints(QPainter::Antialiasing);
 
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
-        brush.setColor(backgroundColor());
+    QBrush brush;
+    brush.setStyle(Qt::SolidPattern);
+    brush.setColor(backgroundColor());
 
-        p.setBrush(brush);
-        p.setPen(Qt::NoPen);
-        p.drawEllipse(square);
+    p.setBrush(brush);
+    p.setPen(Qt::NoPen);
+    p.drawEllipse(square);
 
-        QRect iconGeometry(0, 0, ICON_SIZE, ICON_SIZE);
-        iconGeometry.moveCenter(square.center());
+    QRect iconGeometry(0, 0, ICON_SIZE, ICON_SIZE);
+    iconGeometry.moveCenter(square.center());
 
-        QPixmap pixmap = icon().pixmap(QSize(ICON_SIZE, ICON_SIZE));
-        QPainter icon(&pixmap);
-        icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
-        icon.fillRect(pixmap.rect(), foregroundColor());
+    QPixmap pixmap = icon().pixmap(QSize(ICON_SIZE, ICON_SIZE));
+    QPainter icon(&pixmap);
+    icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
+    icon.fillRect(pixmap.rect(), foregroundColor());
 
-        p.drawPixmap(iconGeometry, pixmap);
+    p.drawPixmap(iconGeometry, pixmap);
 }
diff --git a/src/ui/FloatingButton.h b/src/ui/FloatingButton.h
index b59b385460e4abb5de235c7dc06ff2410aa4e46e..df14dd2cefaa6db1a780ee18ee58ac68457784d1 100644
--- a/src/ui/FloatingButton.h
+++ b/src/ui/FloatingButton.h
@@ -14,17 +14,17 @@ constexpr int OFFSET_Y = 20;
 
 class FloatingButton : public RaisedButton
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        FloatingButton(const QIcon &icon, QWidget *parent = nullptr);
+    FloatingButton(const QIcon &icon, QWidget *parent = nullptr);
 
-        QSize sizeHint() const override { return QSize(DIAMETER, DIAMETER); };
-        QRect buttonGeometry() const;
+    QSize sizeHint() const override { return QSize(DIAMETER, DIAMETER); };
+    QRect buttonGeometry() const;
 
 protected:
-        bool event(QEvent *event) override;
-        bool eventFilter(QObject *obj, QEvent *event) override;
+    bool event(QEvent *event) override;
+    bool eventFilter(QObject *obj, QEvent *event) override;
 
-        void paintEvent(QPaintEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 };
diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp
index ebe0e63fb19e57d64bd301309844be9931bd3303..e238a4d2ddbdfb6de21aefd48e39aa83e10c10a0 100644
--- a/src/ui/InfoMessage.cpp
+++ b/src/ui/InfoMessage.cpp
@@ -19,60 +19,60 @@ constexpr int HMargin  = 20;
 InfoMessage::InfoMessage(QWidget *parent)
   : QWidget{parent}
 {
-        initFont();
+    initFont();
 }
 
 InfoMessage::InfoMessage(QString msg, QWidget *parent)
   : QWidget{parent}
   , msg_{msg}
 {
-        initFont();
+    initFont();
 
-        QFontMetrics fm{font()};
-        width_  = fm.horizontalAdvance(msg_) + HPadding * 2;
-        height_ = fm.ascent() + 2 * VPadding;
+    QFontMetrics fm{font()};
+    width_  = fm.horizontalAdvance(msg_) + HPadding * 2;
+    height_ = fm.ascent() + 2 * VPadding;
 
-        setFixedHeight(height_ + 2 * HMargin);
+    setFixedHeight(height_ + 2 * HMargin);
 }
 
 void
 InfoMessage::paintEvent(QPaintEvent *)
 {
-        QPainter p(this);
-        p.setRenderHint(QPainter::Antialiasing);
-        p.setFont(font());
+    QPainter p(this);
+    p.setRenderHint(QPainter::Antialiasing);
+    p.setFont(font());
 
-        // Center the box horizontally & vertically.
-        auto textRegion = QRectF(width() / 2 - width_ / 2, HMargin, width_, height_);
+    // Center the box horizontally & vertically.
+    auto textRegion = QRectF(width() / 2 - width_ / 2, HMargin, width_, height_);
 
-        QPainterPath ppath;
-        ppath.addRoundedRect(textRegion, height_ / 2, height_ / 2);
+    QPainterPath ppath;
+    ppath.addRoundedRect(textRegion, height_ / 2, height_ / 2);
 
-        p.setPen(Qt::NoPen);
-        p.fillPath(ppath, boxColor());
-        p.drawPath(ppath);
+    p.setPen(Qt::NoPen);
+    p.fillPath(ppath, boxColor());
+    p.drawPath(ppath);
 
-        p.setPen(QPen(textColor()));
-        p.drawText(textRegion, Qt::AlignCenter, msg_);
+    p.setPen(QPen(textColor()));
+    p.drawText(textRegion, Qt::AlignCenter, msg_);
 }
 
 DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent)
   : InfoMessage{parent}
 {
-        auto now = QDateTime::currentDateTime();
+    auto now = QDateTime::currentDateTime();
 
-        QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
+    QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
 
-        if (now.date().year() == datetime.date().year()) {
-                QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
-                fmt = fmt.remove(rx);
-        }
+    if (now.date().year() == datetime.date().year()) {
+        QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
+        fmt = fmt.remove(rx);
+    }
 
-        msg_ = datetime.date().toString(fmt);
+    msg_ = datetime.date().toString(fmt);
 
-        QFontMetrics fm{font()};
-        width_  = fm.horizontalAdvance(msg_) + HPadding * 2;
-        height_ = fm.ascent() + 2 * VPadding;
+    QFontMetrics fm{font()};
+    width_  = fm.horizontalAdvance(msg_) + HPadding * 2;
+    height_ = fm.ascent() + 2 * VPadding;
 
-        setFixedHeight(height_ + 2 * HMargin);
+    setFixedHeight(height_ + 2 * HMargin);
 }
diff --git a/src/ui/InfoMessage.h b/src/ui/InfoMessage.h
index cc0c57dcf3281fb8cfd308d93905438c3839df21..486812a23fa987ddc833db34086f27c7da0fa075 100644
--- a/src/ui/InfoMessage.h
+++ b/src/ui/InfoMessage.h
@@ -10,47 +10,47 @@
 
 class InfoMessage : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor boxColor WRITE setBoxColor READ boxColor)
+    Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
+    Q_PROPERTY(QColor boxColor WRITE setBoxColor READ boxColor)
 
 public:
-        explicit InfoMessage(QWidget *parent = nullptr);
-        InfoMessage(QString msg, QWidget *parent = nullptr);
+    explicit InfoMessage(QWidget *parent = nullptr);
+    InfoMessage(QString msg, QWidget *parent = nullptr);
 
-        void setTextColor(QColor color) { textColor_ = color; }
-        void setBoxColor(QColor color) { boxColor_ = color; }
-        void saveDatetime(QDateTime datetime) { datetime_ = datetime; }
+    void setTextColor(QColor color) { textColor_ = color; }
+    void setBoxColor(QColor color) { boxColor_ = color; }
+    void saveDatetime(QDateTime datetime) { datetime_ = datetime; }
 
-        QColor textColor() const { return textColor_; }
-        QColor boxColor() const { return boxColor_; }
-        QDateTime datetime() const { return datetime_; }
+    QColor textColor() const { return textColor_; }
+    QColor boxColor() const { return boxColor_; }
+    QDateTime datetime() const { return datetime_; }
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
-        void initFont()
-        {
-                QFont f;
-                f.setWeight(QFont::Medium);
-                setFont(f);
-        }
+    void paintEvent(QPaintEvent *event) override;
+    void initFont()
+    {
+        QFont f;
+        f.setWeight(QFont::Medium);
+        setFont(f);
+    }
 
-        int width_;
-        int height_;
+    int width_;
+    int height_;
 
-        QString msg_;
+    QString msg_;
 
-        QDateTime datetime_;
+    QDateTime datetime_;
 
-        QColor textColor_ = QColor("black");
-        QColor boxColor_  = QColor("white");
+    QColor textColor_ = QColor("black");
+    QColor boxColor_  = QColor("white");
 };
 
 class DateSeparator : public InfoMessage
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        DateSeparator(QDateTime datetime, QWidget *parent = nullptr);
+    DateSeparator(QDateTime datetime, QWidget *parent = nullptr);
 };
diff --git a/src/ui/Label.cpp b/src/ui/Label.cpp
index 2e8f8e1d8ada299b332224875f4fe26d37cd164b..220fe2f0bce61a0c678ef98a49dc048bf26534fd 100644
--- a/src/ui/Label.cpp
+++ b/src/ui/Label.cpp
@@ -17,16 +17,16 @@ Label::Label(const QString &text, QWidget *parent, Qt::WindowFlags f)
 void
 Label::mousePressEvent(QMouseEvent *e)
 {
-        pressPosition_ = e->pos();
-        emit pressed(e);
-        QLabel::mousePressEvent(e);
+    pressPosition_ = e->pos();
+    emit pressed(e);
+    QLabel::mousePressEvent(e);
 }
 
 void
 Label::mouseReleaseEvent(QMouseEvent *e)
 {
-        emit released(e);
-        if (pressPosition_ == e->pos())
-                emit clicked(e);
-        QLabel::mouseReleaseEvent(e);
+    emit released(e);
+    if (pressPosition_ == e->pos())
+        emit clicked(e);
+    QLabel::mouseReleaseEvent(e);
 }
diff --git a/src/ui/Label.h b/src/ui/Label.h
index a3eb511b4e9048a0e0ec2d3679f61f1eb37a71c3..b6e76b7766aed869c9bd78a705dd92be061fd821 100644
--- a/src/ui/Label.h
+++ b/src/ui/Label.h
@@ -8,22 +8,22 @@
 
 class Label : public QLabel
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        explicit Label(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
-        explicit Label(const QString &text,
-                       QWidget *parent   = Q_NULLPTR,
-                       Qt::WindowFlags f = Qt::WindowFlags());
+    explicit Label(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
+    explicit Label(const QString &text,
+                   QWidget *parent   = Q_NULLPTR,
+                   Qt::WindowFlags f = Qt::WindowFlags());
 
 signals:
-        void clicked(QMouseEvent *e);
-        void pressed(QMouseEvent *e);
-        void released(QMouseEvent *e);
+    void clicked(QMouseEvent *e);
+    void pressed(QMouseEvent *e);
+    void released(QMouseEvent *e);
 
 protected:
-        void mousePressEvent(QMouseEvent *e) override;
-        void mouseReleaseEvent(QMouseEvent *e) override;
+    void mousePressEvent(QMouseEvent *e) override;
+    void mouseReleaseEvent(QMouseEvent *e) override;
 
-        QPoint pressPosition_;
+    QPoint pressPosition_;
 };
diff --git a/src/ui/LoadingIndicator.cpp b/src/ui/LoadingIndicator.cpp
index fb3c761cc40ab875b49ce4423fb8e52cac52e4bd..7581ec83bb19befe9072b8c84e7116e2336df89b 100644
--- a/src/ui/LoadingIndicator.cpp
+++ b/src/ui/LoadingIndicator.cpp
@@ -14,70 +14,70 @@ LoadingIndicator::LoadingIndicator(QWidget *parent)
   , angle_(0)
   , color_(Qt::black)
 {
-        setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
-        setFocusPolicy(Qt::NoFocus);
+    setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+    setFocusPolicy(Qt::NoFocus);
 
-        timer_ = new QTimer(this);
-        connect(timer_, SIGNAL(timeout()), this, SLOT(onTimeout()));
+    timer_ = new QTimer(this);
+    connect(timer_, SIGNAL(timeout()), this, SLOT(onTimeout()));
 }
 
 void
 LoadingIndicator::paintEvent(QPaintEvent *e)
 {
-        Q_UNUSED(e)
+    Q_UNUSED(e)
 
-        if (!timer_->isActive())
-                return;
+    if (!timer_->isActive())
+        return;
 
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
 
-        int width = qMin(this->width(), this->height());
+    int width = qMin(this->width(), this->height());
 
-        int outerRadius = (width - 4) * 0.5f;
-        int innerRadius = outerRadius * 0.78f;
+    int outerRadius = (width - 4) * 0.5f;
+    int innerRadius = outerRadius * 0.78f;
 
-        int capsuleRadius = (outerRadius - innerRadius) / 2;
+    int capsuleRadius = (outerRadius - innerRadius) / 2;
 
-        for (int i = 0; i < 8; ++i) {
-                QColor color = color_;
+    for (int i = 0; i < 8; ++i) {
+        QColor color = color_;
 
-                color.setAlphaF(1.0f - (i / 8.0f));
+        color.setAlphaF(1.0f - (i / 8.0f));
 
-                painter.setPen(Qt::NoPen);
-                painter.setBrush(color);
+        painter.setPen(Qt::NoPen);
+        painter.setBrush(color);
 
-                qreal radius = capsuleRadius * (1.0f - (i / 16.0f));
+        qreal radius = capsuleRadius * (1.0f - (i / 16.0f));
 
-                painter.save();
+        painter.save();
 
-                painter.translate(rect().center());
-                painter.rotate(angle_ - i * 45.0f);
+        painter.translate(rect().center());
+        painter.rotate(angle_ - i * 45.0f);
 
-                QPointF center = QPointF(-capsuleRadius, -innerRadius);
-                painter.drawEllipse(center, radius * 2, radius * 2);
+        QPointF center = QPointF(-capsuleRadius, -innerRadius);
+        painter.drawEllipse(center, radius * 2, radius * 2);
 
-                painter.restore();
-        }
+        painter.restore();
+    }
 }
 
 void
 LoadingIndicator::start()
 {
-        timer_->start(interval_);
-        show();
+    timer_->start(interval_);
+    show();
 }
 
 void
 LoadingIndicator::stop()
 {
-        timer_->stop();
-        hide();
+    timer_->stop();
+    hide();
 }
 
 void
 LoadingIndicator::onTimeout()
 {
-        angle_ = (angle_ + 45) % 360;
-        repaint();
+    angle_ = (angle_ + 45) % 360;
+    repaint();
 }
diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h
index ba56b449b8b1b52ffa4d11e5fcd71e0ee1d913e7..458ecd408070621ff3644fb01c92bdfbd30f63d2 100644
--- a/src/ui/LoadingIndicator.h
+++ b/src/ui/LoadingIndicator.h
@@ -12,30 +12,30 @@ class QTimer;
 class QPaintEvent;
 class LoadingIndicator : public QWidget
 {
-        Q_OBJECT
-        Q_PROPERTY(QColor color READ color WRITE setColor)
+    Q_OBJECT
+    Q_PROPERTY(QColor color READ color WRITE setColor)
 
 public:
-        LoadingIndicator(QWidget *parent = nullptr);
+    LoadingIndicator(QWidget *parent = nullptr);
 
-        void paintEvent(QPaintEvent *e) override;
+    void paintEvent(QPaintEvent *e) override;
 
-        void start();
-        void stop();
+    void start();
+    void stop();
 
-        QColor color() { return color_; }
-        void setColor(QColor color) { color_ = color; }
+    QColor color() { return color_; }
+    void setColor(QColor color) { color_ = color; }
 
-        int interval() { return interval_; }
-        void setInterval(int interval) { interval_ = interval; }
+    int interval() { return interval_; }
+    void setInterval(int interval) { interval_ = interval; }
 
 private slots:
-        void onTimeout();
+    void onTimeout();
 
 private:
-        int interval_;
-        int angle_;
+    int interval_;
+    int angle_;
 
-        QColor color_;
-        QTimer *timer_;
+    QColor color_;
+    QTimer *timer_;
 };
diff --git a/src/ui/Menu.h b/src/ui/Menu.h
index fd2946ddb5d5270279ac87fb00f0fcb4569445aa..d1ac2b80e91b3f3baae43fc4f0f447b5ab901c2c 100644
--- a/src/ui/Menu.h
+++ b/src/ui/Menu.h
@@ -10,16 +10,16 @@
 
 class Menu : public QMenu
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        Menu(QWidget *parent = nullptr)
-          : QMenu(parent){};
+    Menu(QWidget *parent = nullptr)
+      : QMenu(parent){};
 
 protected:
-        void leaveEvent(QEvent *e) override
-        {
-                hide();
+    void leaveEvent(QEvent *e) override
+    {
+        hide();
 
-                QMenu::leaveEvent(e);
-        }
+        QMenu::leaveEvent(e);
+    }
 };
diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..72758653ef8c1f6acc1622c5b7eee2e857b01e6a
--- /dev/null
+++ b/src/ui/MxcAnimatedImage.cpp
@@ -0,0 +1,175 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "MxcAnimatedImage.h"
+
+#include <QDir>
+#include <QFileInfo>
+#include <QMimeDatabase>
+#include <QQuickWindow>
+#include <QSGImageNode>
+#include <QStandardPaths>
+
+#include "EventAccessors.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "timeline/TimelineModel.h"
+
+void
+MxcAnimatedImage::startDownload()
+{
+    if (!room_)
+        return;
+    if (eventId_.isEmpty())
+        return;
+
+    auto event = room_->eventById(eventId_);
+    if (!event) {
+        nhlog::ui()->error("Failed to load media for event {}, event not found.",
+                           eventId_.toStdString());
+        return;
+    }
+
+    QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8();
+
+    animatable_ = QMovie::supportedFormats().contains(mimeType.split('/').back());
+    animatableChanged();
+
+    if (!animatable_)
+        return;
+
+    QString mxcUrl           = QString::fromStdString(mtx::accessors::url(*event));
+    QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
+
+    auto encryptionInfo = mtx::accessors::file(*event);
+
+    // If the message is a link to a non mxcUrl, don't download it
+    if (!mxcUrl.startsWith("mxc://")) {
+        return;
+    }
+
+    QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
+
+    const auto url  = mxcUrl.toStdString();
+    const auto name = QString(mxcUrl).remove("mxc://");
+    QFileInfo filename(QString("%1/media_cache/media/%2.%3")
+                         .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                         .arg(name)
+                         .arg(suffix));
+    if (QDir::cleanPath(name) != name) {
+        nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
+        return;
+    }
+
+    QDir().mkpath(filename.path());
+
+    QPointer<MxcAnimatedImage> self = this;
+
+    auto processBuffer = [this, mimeType, encryptionInfo, self](QIODevice &device) {
+        if (!self)
+            return;
+
+        if (buffer.isOpen()) {
+            movie.stop();
+            movie.setDevice(nullptr);
+            buffer.close();
+        }
+
+        if (encryptionInfo) {
+            QByteArray ba = device.readAll();
+            std::string temp(ba.constData(), ba.size());
+            temp = mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+            buffer.setData(temp.data(), temp.size());
+        } else {
+            buffer.setData(device.readAll());
+        }
+        buffer.open(QIODevice::ReadOnly);
+        buffer.reset();
+
+        QTimer::singleShot(0, this, [this, mimeType] {
+            nhlog::ui()->info(
+              "Playing movie with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
+            movie.setFormat(mimeType);
+            movie.setDevice(&buffer);
+            if (play_)
+                movie.start();
+            else
+                movie.jumpToFrame(0);
+            emit loadedChanged();
+            update();
+        });
+    };
+
+    if (filename.isReadable()) {
+        QFile f(filename.filePath());
+        if (f.open(QIODevice::ReadOnly)) {
+            processBuffer(f);
+            return;
+        }
+    }
+
+    http::client()->download(url,
+                             [filename, url, processBuffer](const std::string &data,
+                                                            const std::string &,
+                                                            const std::string &,
+                                                            mtx::http::RequestErr err) {
+                                 if (err) {
+                                     nhlog::net()->warn("failed to retrieve media {}: {} {}",
+                                                        url,
+                                                        err->matrix_error.error,
+                                                        static_cast<int>(err->status_code));
+                                     return;
+                                 }
+
+                                 try {
+                                     QFile file(filename.filePath());
+
+                                     if (!file.open(QIODevice::WriteOnly))
+                                         return;
+
+                                     QByteArray ba(data.data(), (int)data.size());
+                                     file.write(ba);
+                                     file.close();
+
+                                     QBuffer buf(&ba);
+                                     buf.open(QBuffer::ReadOnly);
+                                     processBuffer(buf);
+                                 } catch (const std::exception &e) {
+                                     nhlog::ui()->warn("Error while saving file to: {}", e.what());
+                                 }
+                             });
+}
+
+QSGNode *
+MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
+{
+    if (!imageDirty)
+        return oldNode;
+
+    imageDirty      = false;
+    QSGImageNode *n = static_cast<QSGImageNode *>(oldNode);
+    if (!n) {
+        n = window()->createImageNode();
+        n->setOwnsTexture(true);
+        // n->setFlags(QSGNode::OwnedByParent | QSGNode::OwnsGeometry |
+        // GSGNode::OwnsMaterial);
+        n->setFlags(QSGNode::OwnedByParent);
+    }
+
+    // n->setTexture(nullptr);
+    auto img = movie.currentImage();
+    if (!img.isNull())
+        n->setTexture(window()->createTextureFromImage(img));
+    else {
+        delete n;
+        return nullptr;
+    }
+
+    n->setSourceRect(img.rect());
+    n->setRect(QRect(0, 0, width(), height()));
+    n->setFiltering(QSGTexture::Linear);
+    n->setMipmapFiltering(QSGTexture::Linear);
+
+    return n;
+}
diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h
new file mode 100644
index 0000000000000000000000000000000000000000..c3ca24d1cb6dbe5c6832465eed769645dd76ed6a
--- /dev/null
+++ b/src/ui/MxcAnimatedImage.h
@@ -0,0 +1,91 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QBuffer>
+#include <QMovie>
+#include <QObject>
+#include <QQuickItem>
+
+class TimelineModel;
+
+// This is an AnimatedImage, that can draw encrypted images
+class MxcAnimatedImage : public QQuickItem
+{
+    Q_OBJECT
+    Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
+    Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged)
+    Q_PROPERTY(bool animatable READ animatable NOTIFY animatableChanged)
+    Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged)
+    Q_PROPERTY(bool play READ play WRITE setPlay NOTIFY playChanged)
+public:
+    MxcAnimatedImage(QQuickItem *parent = nullptr)
+      : QQuickItem(parent)
+    {
+        connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload);
+        connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload);
+        connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame);
+        setFlag(QQuickItem::ItemHasContents);
+        // setAcceptHoverEvents(true);
+    }
+
+    bool animatable() const { return animatable_; }
+    bool loaded() const { return buffer.size() > 0; }
+    bool play() const { return play_; }
+    QString eventId() const { return eventId_; }
+    TimelineModel *room() const { return room_; }
+    void setEventId(QString newEventId)
+    {
+        if (eventId_ != newEventId) {
+            eventId_ = newEventId;
+            emit eventIdChanged();
+        }
+    }
+    void setRoom(TimelineModel *room)
+    {
+        if (room_ != room) {
+            room_ = room;
+            emit roomChanged();
+        }
+    }
+    void setPlay(bool newPlay)
+    {
+        if (play_ != newPlay) {
+            play_ = newPlay;
+            movie.setPaused(!play_);
+            emit playChanged();
+        }
+    }
+
+    QSGNode *updatePaintNode(QSGNode *oldNode,
+                             QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;
+
+signals:
+    void roomChanged();
+    void eventIdChanged();
+    void animatableChanged();
+    void loadedChanged();
+    void playChanged();
+
+private slots:
+    void startDownload();
+    void newFrame(int frame)
+    {
+        currentFrame = frame;
+        imageDirty   = true;
+        update();
+    }
+
+private:
+    TimelineModel *room_ = nullptr;
+    QString eventId_;
+    QString filename_;
+    bool animatable_ = false;
+    QBuffer buffer;
+    QMovie movie;
+    int currentFrame = 0;
+    bool imageDirty  = true;
+    bool play_       = true;
+};
diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..db8c0f1f36e24de21fc1487191e25be6a7a90032
--- /dev/null
+++ b/src/ui/MxcMediaProxy.cpp
@@ -0,0 +1,139 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "MxcMediaProxy.h"
+
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QMediaObject>
+#include <QMediaPlayer>
+#include <QMimeDatabase>
+#include <QStandardPaths>
+#include <QUrl>
+
+#include "EventAccessors.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "timeline/TimelineModel.h"
+
+void
+MxcMediaProxy::setVideoSurface(QAbstractVideoSurface *surface)
+{
+    qDebug() << "Changing surface";
+    m_surface = surface;
+    setVideoOutput(m_surface);
+}
+
+QAbstractVideoSurface *
+MxcMediaProxy::getVideoSurface()
+{
+    return m_surface;
+}
+
+void
+MxcMediaProxy::startDownload()
+{
+    if (!room_)
+        return;
+    if (eventId_.isEmpty())
+        return;
+
+    auto event = room_->eventById(eventId_);
+    if (!event) {
+        nhlog::ui()->error("Failed to load media for event {}, event not found.",
+                           eventId_.toStdString());
+        return;
+    }
+
+    QString mxcUrl           = QString::fromStdString(mtx::accessors::url(*event));
+    QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
+    QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(*event));
+
+    auto encryptionInfo = mtx::accessors::file(*event);
+
+    // If the message is a link to a non mxcUrl, don't download it
+    if (!mxcUrl.startsWith("mxc://")) {
+        return;
+    }
+
+    QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
+
+    const auto url  = mxcUrl.toStdString();
+    const auto name = QString(mxcUrl).remove("mxc://");
+    QFileInfo filename(QString("%1/media_cache/media/%2.%3")
+                         .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                         .arg(name)
+                         .arg(suffix));
+    if (QDir::cleanPath(name) != name) {
+        nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
+        return;
+    }
+
+    QDir().mkpath(filename.path());
+
+    QPointer<MxcMediaProxy> self = this;
+
+    auto processBuffer = [this, encryptionInfo, filename, self](QIODevice &device) {
+        if (!self)
+            return;
+
+        if (encryptionInfo) {
+            QByteArray ba = device.readAll();
+            std::string temp(ba.constData(), ba.size());
+            temp = mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+            buffer.setData(temp.data(), temp.size());
+        } else {
+            buffer.setData(device.readAll());
+        }
+        buffer.open(QIODevice::ReadOnly);
+        buffer.reset();
+
+        QTimer::singleShot(0, this, [this, filename] {
+            nhlog::ui()->info(
+              "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
+            this->setMedia(QMediaContent(filename.fileName()), &buffer);
+            emit loadedChanged();
+        });
+    };
+
+    if (filename.isReadable()) {
+        QFile f(filename.filePath());
+        if (f.open(QIODevice::ReadOnly)) {
+            processBuffer(f);
+            return;
+        }
+    }
+
+    http::client()->download(url,
+                             [filename, url, processBuffer](const std::string &data,
+                                                            const std::string &,
+                                                            const std::string &,
+                                                            mtx::http::RequestErr err) {
+                                 if (err) {
+                                     nhlog::net()->warn("failed to retrieve media {}: {} {}",
+                                                        url,
+                                                        err->matrix_error.error,
+                                                        static_cast<int>(err->status_code));
+                                     return;
+                                 }
+
+                                 try {
+                                     QFile file(filename.filePath());
+
+                                     if (!file.open(QIODevice::WriteOnly))
+                                         return;
+
+                                     QByteArray ba(data.data(), (int)data.size());
+                                     file.write(ba);
+                                     file.close();
+
+                                     QBuffer buf(&ba);
+                                     buf.open(QBuffer::ReadOnly);
+                                     processBuffer(buf);
+                                 } catch (const std::exception &e) {
+                                     nhlog::ui()->warn("Error while saving file to: {}", e.what());
+                                 }
+                             });
+}
diff --git a/src/ui/MxcMediaProxy.h b/src/ui/MxcMediaProxy.h
new file mode 100644
index 0000000000000000000000000000000000000000..18152c756030ca130677fb8e82c2e4d4332dec94
--- /dev/null
+++ b/src/ui/MxcMediaProxy.h
@@ -0,0 +1,77 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractVideoSurface>
+#include <QBuffer>
+#include <QMediaContent>
+#include <QMediaPlayer>
+#include <QObject>
+#include <QPointer>
+#include <QString>
+
+#include "Logging.h"
+
+class TimelineModel;
+
+// I failed to get my own buffer into the MediaPlayer in qml, so just make our own. For that we just
+// need the videoSurface property, so that part is really easy!
+class MxcMediaProxy : public QMediaPlayer
+{
+    Q_OBJECT
+    Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
+    Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged)
+    Q_PROPERTY(QAbstractVideoSurface *videoSurface READ getVideoSurface WRITE setVideoSurface)
+    Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged)
+public:
+    MxcMediaProxy(QObject *parent = nullptr)
+      : QMediaPlayer(parent)
+    {
+        connect(this, &MxcMediaProxy::eventIdChanged, &MxcMediaProxy::startDownload);
+        connect(this, &MxcMediaProxy::roomChanged, &MxcMediaProxy::startDownload);
+        connect(this,
+                qOverload<QMediaPlayer::Error>(&MxcMediaProxy::error),
+                [this](QMediaPlayer::Error error) {
+                    nhlog::ui()->info("Media player error {} and errorStr {}",
+                                      error,
+                                      this->errorString().toStdString());
+                });
+        connect(this, &MxcMediaProxy::mediaStatusChanged, [this](QMediaPlayer::MediaStatus status) {
+            nhlog::ui()->info("Media player status {} and error {}", status, this->error());
+        });
+    }
+
+    bool loaded() const { return buffer.size() > 0; }
+    QString eventId() const { return eventId_; }
+    TimelineModel *room() const { return room_; }
+    void setEventId(QString newEventId)
+    {
+        eventId_ = newEventId;
+        emit eventIdChanged();
+    }
+    void setRoom(TimelineModel *room)
+    {
+        room_ = room;
+        emit roomChanged();
+    }
+    void setVideoSurface(QAbstractVideoSurface *surface);
+    QAbstractVideoSurface *getVideoSurface();
+
+signals:
+    void roomChanged();
+    void eventIdChanged();
+    void loadedChanged();
+    void newBuffer(QMediaContent, QIODevice *buf);
+
+private slots:
+    void startDownload();
+
+private:
+    TimelineModel *room_ = nullptr;
+    QString eventId_;
+    QString filename_;
+    QBuffer buffer;
+    QAbstractVideoSurface *m_surface = nullptr;
+};
diff --git a/src/ui/NhekoCursorShape.cpp b/src/ui/NhekoCursorShape.cpp
index b36eedbd74e8f95bb86aa98eea0c712ce6559934..7099175746c838879f2590d28d2bb8d4eb50989e 100644
--- a/src/ui/NhekoCursorShape.cpp
+++ b/src/ui/NhekoCursorShape.cpp
@@ -14,16 +14,16 @@ NhekoCursorShape::NhekoCursorShape(QQuickItem *parent)
 Qt::CursorShape
 NhekoCursorShape::cursorShape() const
 {
-        return cursor().shape();
+    return cursor().shape();
 }
 
 void
 NhekoCursorShape::setCursorShape(Qt::CursorShape cursorShape)
 {
-        if (currentShape_ == cursorShape)
-                return;
+    if (currentShape_ == cursorShape)
+        return;
 
-        currentShape_ = cursorShape;
-        setCursor(cursorShape);
-        emit cursorShapeChanged();
+    currentShape_ = cursorShape;
+    setCursor(cursorShape);
+    emit cursorShapeChanged();
 }
diff --git a/src/ui/NhekoCursorShape.h b/src/ui/NhekoCursorShape.h
index 6f6a2b826c6330eb82fd56b4e14e004f3e289a95..b3a0a1baf887d6258d89339cf2814f26bba3a944 100644
--- a/src/ui/NhekoCursorShape.h
+++ b/src/ui/NhekoCursorShape.h
@@ -11,20 +11,20 @@
 
 class NhekoCursorShape : public QQuickItem
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY
-                     cursorShapeChanged)
+    Q_PROPERTY(
+      Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY cursorShapeChanged)
 
 public:
-        explicit NhekoCursorShape(QQuickItem *parent = 0);
+    explicit NhekoCursorShape(QQuickItem *parent = 0);
 
 private:
-        Qt::CursorShape cursorShape() const;
-        void setCursorShape(Qt::CursorShape cursorShape);
+    Qt::CursorShape cursorShape() const;
+    void setCursorShape(Qt::CursorShape cursorShape);
 
-        Qt::CursorShape currentShape_;
+    Qt::CursorShape currentShape_;
 
 signals:
-        void cursorShapeChanged();
+    void cursorShapeChanged();
 };
diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp
index bbcedd7ebd276656dda1fbd3f9a7a38ba36a997c..b1b53c3d0fcfe3c5a3db0a6c7292d6c8f47a2af2 100644
--- a/src/ui/NhekoDropArea.cpp
+++ b/src/ui/NhekoDropArea.cpp
@@ -16,28 +16,28 @@
 NhekoDropArea::NhekoDropArea(QQuickItem *parent)
   : QQuickItem(parent)
 {
-        setFlags(ItemAcceptsDrops);
+    setFlags(ItemAcceptsDrops);
 }
 
 void
 NhekoDropArea::dragEnterEvent(QDragEnterEvent *event)
 {
-        event->acceptProposedAction();
+    event->acceptProposedAction();
 }
 
 void
 NhekoDropArea::dragMoveEvent(QDragMoveEvent *event)
 {
-        event->acceptProposedAction();
+    event->acceptProposedAction();
 }
 
 void
 NhekoDropArea::dropEvent(QDropEvent *event)
 {
-        if (event) {
-                auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_);
-                if (model) {
-                        model->input()->insertMimeData(event->mimeData());
-                }
+    if (event) {
+        auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_);
+        if (model) {
+            model->input()->insertMimeData(event->mimeData());
         }
+    }
 }
diff --git a/src/ui/NhekoDropArea.h b/src/ui/NhekoDropArea.h
index 9fbf173716ea58f92623565887e4d1979008ec50..7b3de5c90f17cb785d8ed0146c00428d659c0db2 100644
--- a/src/ui/NhekoDropArea.h
+++ b/src/ui/NhekoDropArea.h
@@ -6,29 +6,29 @@
 
 class NhekoDropArea : public QQuickItem
 {
-        Q_OBJECT
-        Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged)
+    Q_OBJECT
+    Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged)
 public:
-        NhekoDropArea(QQuickItem *parent = nullptr);
+    NhekoDropArea(QQuickItem *parent = nullptr);
 
 signals:
-        void roomidChanged(QString roomid);
+    void roomidChanged(QString roomid);
 
 public slots:
-        void setRoomid(QString roomid)
-        {
-                if (roomid_ != roomid) {
-                        roomid_ = roomid;
-                        emit roomidChanged(roomid);
-                }
+    void setRoomid(QString roomid)
+    {
+        if (roomid_ != roomid) {
+            roomid_ = roomid;
+            emit roomidChanged(roomid);
         }
-        QString roomid() const { return roomid_; }
+    }
+    QString roomid() const { return roomid_; }
 
 protected:
-        void dragEnterEvent(QDragEnterEvent *event) override;
-        void dragMoveEvent(QDragMoveEvent *event) override;
-        void dropEvent(QDropEvent *event) override;
+    void dragEnterEvent(QDragEnterEvent *event) override;
+    void dragMoveEvent(QDragMoveEvent *event) override;
+    void dropEvent(QDropEvent *event) override;
 
 private:
-        QString roomid_;
+    QString roomid_;
 };
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index 9e0d706bd35d61dae522567b7b78b5af0b44a6f5..a93466d20f42856781bc799a5f704df360dc9fb1 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -14,136 +14,101 @@
 #include "MainWindow.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "voip/WebRTCSession.h"
 
 Nheko::Nheko()
 {
-        connect(
-          UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged);
-        connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile);
+    connect(
+      UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged);
+    connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile);
+    connect(this, &Nheko::joinRoom, ChatPage::instance(), &ChatPage::joinRoom);
 }
 
 void
 Nheko::updateUserProfile()
 {
-        if (cache::client() && cache::client()->isInitialized())
-                currentUser_.reset(
-                  new UserProfile("", utils::localUser(), ChatPage::instance()->timelineManager()));
-        else
-                currentUser_.reset();
-        emit profileChanged();
+    if (cache::client() && cache::client()->isInitialized())
+        currentUser_.reset(
+          new UserProfile("", utils::localUser(), ChatPage::instance()->timelineManager()));
+    else
+        currentUser_.reset();
+    emit profileChanged();
 }
 
 QPalette
 Nheko::colors() const
 {
-        return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString());
+    return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString());
 }
 
 QPalette
 Nheko::inactiveColors() const
 {
-        auto p = colors();
-        p.setCurrentColorGroup(QPalette::ColorGroup::Inactive);
-        return p;
+    auto p = colors();
+    p.setCurrentColorGroup(QPalette::ColorGroup::Inactive);
+    return p;
 }
 
 Theme
 Nheko::theme() const
 {
-        return Theme(UserSettings::instance()->theme().toStdString());
+    return Theme(UserSettings::instance()->theme().toStdString());
 }
 
 void
 Nheko::openLink(QString link) const
 {
-        QUrl url(link);
-        if (url.scheme() == "https" && url.host() == "matrix.to") {
-                // handle matrix.to links internally
-                QString p = url.fragment(QUrl::FullyEncoded);
-                if (p.startsWith("/"))
-                        p.remove(0, 1);
-
-                auto temp = p.split("?");
-                QString query;
-                if (temp.size() >= 2)
-                        query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
-
-                temp            = temp.first().split("/");
-                auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
-                QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
-                if (!identifier.isEmpty()) {
-                        if (identifier.startsWith("@")) {
-                                QByteArray uri =
-                                  "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
-                                if (!query.isEmpty())
-                                        uri.append("?" + query.toUtf8());
-                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
-                        } else if (identifier.startsWith("#")) {
-                                QByteArray uri =
-                                  "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
-                                if (!eventId.isEmpty())
-                                        uri.append("/e/" +
-                                                   QUrl::toPercentEncoding(eventId.remove(0, 1)));
-                                if (!query.isEmpty())
-                                        uri.append("?" + query.toUtf8());
-                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
-                        } else if (identifier.startsWith("!")) {
-                                QByteArray uri = "matrix:roomid/" +
-                                                 QUrl::toPercentEncoding(identifier.remove(0, 1));
-                                if (!eventId.isEmpty())
-                                        uri.append("/e/" +
-                                                   QUrl::toPercentEncoding(eventId.remove(0, 1)));
-                                if (!query.isEmpty())
-                                        uri.append("?" + query.toUtf8());
-                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
-                        }
-                }
-        } else {
-                QDesktopServices::openUrl(url);
-        }
+    QUrl url(link);
+    // Open externally if we couldn't handle it internally
+    if (!ChatPage::instance()->handleMatrixUri(url)) {
+        const QStringList allowedUrlSchemes = {
+          "http",
+          "https",
+          "mailto",
+        };
+
+        if (allowedUrlSchemes.contains(url.scheme()))
+            QDesktopServices::openUrl(url);
+        else
+            nhlog::ui()->warn("Url '{}' not opened, because the scheme is not in the allow list",
+                              url.toDisplayString().toStdString());
+    }
 }
 void
 Nheko::setStatusMessage(QString msg) const
 {
-        ChatPage::instance()->setStatus(msg);
+    ChatPage::instance()->setStatus(msg);
 }
 
 UserProfile *
 Nheko::currentUser() const
 {
-        nhlog::ui()->debug("Profile requested");
+    nhlog::ui()->debug("Profile requested");
 
-        return currentUser_.get();
+    return currentUser_.get();
 }
 
 void
 Nheko::showUserSettingsPage() const
 {
-        ChatPage::instance()->showUserSettingsPage();
+    ChatPage::instance()->showUserSettingsPage();
 }
 
 void
-Nheko::openLogoutDialog() const
+Nheko::logout() const
 {
-        MainWindow::instance()->openLogoutDialog();
+    ChatPage::instance()->initiateLogout();
 }
 
 void
 Nheko::openCreateRoomDialog() const
 {
-        MainWindow::instance()->openCreateRoomDialog(
-          [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); });
-}
-
-void
-Nheko::openJoinRoomDialog() const
-{
-        MainWindow::instance()->openJoinRoomDialog(
-          [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
+    MainWindow::instance()->openCreateRoomDialog(
+      [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); });
 }
 
 void
 Nheko::reparent(QWindow *win) const
 {
-        win->setTransientParent(MainWindow::instance()->windowHandle());
+    win->setTransientParent(MainWindow::instance()->windowHandle());
 }
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index d4d119dc6be062853b8c2f32a9d13dd52792073f..c70813c5d7184f6455db92150b7df7c92d90fbc6 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -15,51 +15,54 @@ class QWindow;
 
 class Nheko : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
-        Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
-        Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged)
-        Q_PROPERTY(int avatarSize READ avatarSize CONSTANT)
-        Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT)
-        Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT)
-        Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT)
+    Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
+    Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
+    Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged)
+    Q_PROPERTY(int avatarSize READ avatarSize CONSTANT)
+    Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT)
+    Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT)
+    Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT)
 
-        Q_PROPERTY(UserProfile *currentUser READ currentUser NOTIFY profileChanged)
+    Q_PROPERTY(UserProfile *currentUser READ currentUser NOTIFY profileChanged)
 
 public:
-        Nheko();
-
-        QPalette colors() const;
-        QPalette inactiveColors() const;
-        Theme theme() const;
-
-        int avatarSize() const { return 40; }
-
-        int paddingSmall() const { return 4; }
-        int paddingMedium() const { return 8; }
-        int paddingLarge() const { return 20; }
-        UserProfile *currentUser() const;
-
-        Q_INVOKABLE QFont monospaceFont() const
-        {
-                return QFontDatabase::systemFont(QFontDatabase::FixedFont);
-        }
-        Q_INVOKABLE void openLink(QString link) const;
-        Q_INVOKABLE void setStatusMessage(QString msg) const;
-        Q_INVOKABLE void showUserSettingsPage() const;
-        Q_INVOKABLE void openLogoutDialog() const;
-        Q_INVOKABLE void openCreateRoomDialog() const;
-        Q_INVOKABLE void openJoinRoomDialog() const;
-        Q_INVOKABLE void reparent(QWindow *win) const;
+    Nheko();
+
+    QPalette colors() const;
+    QPalette inactiveColors() const;
+    Theme theme() const;
+
+    int avatarSize() const { return 40; }
+
+    int paddingSmall() const { return 4; }
+    int paddingMedium() const { return 8; }
+    int paddingLarge() const { return 20; }
+    UserProfile *currentUser() const;
+
+    Q_INVOKABLE QFont monospaceFont() const
+    {
+        return QFontDatabase::systemFont(QFontDatabase::FixedFont);
+    }
+    Q_INVOKABLE void openLink(QString link) const;
+    Q_INVOKABLE void setStatusMessage(QString msg) const;
+    Q_INVOKABLE void showUserSettingsPage() const;
+    Q_INVOKABLE void logout() const;
+    Q_INVOKABLE void openCreateRoomDialog() const;
+    Q_INVOKABLE void reparent(QWindow *win) const;
 
 public slots:
-        void updateUserProfile();
+    void updateUserProfile();
 
 signals:
-        void colorsChanged();
-        void profileChanged();
+    void colorsChanged();
+    void profileChanged();
+
+    void openLogoutDialog();
+    void openJoinRoomDialog();
+    void joinRoom(QString roomId);
 
 private:
-        QScopedPointer<UserProfile> currentUser_;
+    QScopedPointer<UserProfile> currentUser_;
 };
diff --git a/src/ui/OverlayModal.cpp b/src/ui/OverlayModal.cpp
index f5f287322c896e6c87ecfe0cfa69e41d12e42474..6534c4bc08c929f4cf4bfecf9e699e84cd570d09 100644
--- a/src/ui/OverlayModal.cpp
+++ b/src/ui/OverlayModal.cpp
@@ -12,50 +12,50 @@ OverlayModal::OverlayModal(QWidget *parent)
   : OverlayWidget(parent)
   , color_{QColor(30, 30, 30, 170)}
 {
-        layout_ = new QVBoxLayout(this);
-        layout_->setSpacing(0);
-        layout_->setContentsMargins(10, 40, 10, 20);
-        setContentAlignment(Qt::AlignCenter);
+    layout_ = new QVBoxLayout(this);
+    layout_->setSpacing(0);
+    layout_->setContentsMargins(10, 40, 10, 20);
+    setContentAlignment(Qt::AlignCenter);
 }
 
 void
 OverlayModal::setWidget(QWidget *widget)
 {
-        // Delete the previous widget
-        if (layout_->count() > 0) {
-                QLayoutItem *item;
-                while ((item = layout_->takeAt(0)) != nullptr) {
-                        delete item->widget();
-                        delete item;
-                }
+    // Delete the previous widget
+    if (layout_->count() > 0) {
+        QLayoutItem *item;
+        while ((item = layout_->takeAt(0)) != nullptr) {
+            delete item->widget();
+            delete item;
         }
+    }
 
-        layout_->addWidget(widget);
-        content_ = widget;
-        content_->setFocus();
+    layout_->addWidget(widget);
+    content_ = widget;
+    content_->setFocus();
 }
 
 void
 OverlayModal::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event);
+    Q_UNUSED(event);
 
-        QPainter painter(this);
-        painter.fillRect(rect(), color_);
+    QPainter painter(this);
+    painter.fillRect(rect(), color_);
 }
 
 void
 OverlayModal::mousePressEvent(QMouseEvent *e)
 {
-        if (isDismissible_ && content_ && !content_->geometry().contains(e->pos()))
-                hide();
+    if (isDismissible_ && content_ && !content_->geometry().contains(e->pos()))
+        hide();
 }
 
 void
 OverlayModal::keyPressEvent(QKeyEvent *event)
 {
-        if (event->key() == Qt::Key_Escape) {
-                event->accept();
-                hide();
-        }
+    if (event->key() == Qt::Key_Escape) {
+        event->accept();
+        hide();
+    }
 }
diff --git a/src/ui/OverlayModal.h b/src/ui/OverlayModal.h
index 005614fa56962f08db7746e565e0e6cd64eb22fc..5c15f17f63c210b088ec4164a626ddf9e347bb94 100644
--- a/src/ui/OverlayModal.h
+++ b/src/ui/OverlayModal.h
@@ -15,25 +15,25 @@
 class OverlayModal : public OverlayWidget
 {
 public:
-        OverlayModal(QWidget *parent);
+    OverlayModal(QWidget *parent);
 
-        void setColor(QColor color) { color_ = color; }
-        void setDismissible(bool state) { isDismissible_ = state; }
+    void setColor(QColor color) { color_ = color; }
+    void setDismissible(bool state) { isDismissible_ = state; }
 
-        void setContentAlignment(QFlags<Qt::AlignmentFlag> flag) { layout_->setAlignment(flag); }
-        void setWidget(QWidget *widget);
+    void setContentAlignment(QFlags<Qt::AlignmentFlag> flag) { layout_->setAlignment(flag); }
+    void setWidget(QWidget *widget);
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
-        void keyPressEvent(QKeyEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
+    void keyPressEvent(QKeyEvent *event) override;
+    void mousePressEvent(QMouseEvent *event) override;
 
 private:
-        QWidget *content_;
-        QVBoxLayout *layout_;
+    QWidget *content_;
+    QVBoxLayout *layout_;
 
-        QColor color_;
+    QColor color_;
 
-        //! Decides whether or not the modal can be removed by clicking into it.
-        bool isDismissible_ = true;
+    //! Decides whether or not the modal can be removed by clicking into it.
+    bool isDismissible_ = true;
 };
diff --git a/src/ui/OverlayWidget.cpp b/src/ui/OverlayWidget.cpp
index c8c95581b29c554222ff6353018058c8396a6066..4e338753ca1e17c7ed5da89473a68414686a1ce1 100644
--- a/src/ui/OverlayWidget.cpp
+++ b/src/ui/OverlayWidget.cpp
@@ -10,69 +10,69 @@
 OverlayWidget::OverlayWidget(QWidget *parent)
   : QWidget(parent)
 {
-        if (parent) {
-                parent->installEventFilter(this);
-                setGeometry(overlayGeometry());
-                raise();
-        }
+    if (parent) {
+        parent->installEventFilter(this);
+        setGeometry(overlayGeometry());
+        raise();
+    }
 }
 
 bool
 OverlayWidget::event(QEvent *event)
 {
-        if (!parent())
-                return QWidget::event(event);
+    if (!parent())
+        return QWidget::event(event);
 
-        switch (event->type()) {
-        case QEvent::ParentChange: {
-                parent()->installEventFilter(this);
-                setGeometry(overlayGeometry());
-                break;
-        }
-        case QEvent::ParentAboutToChange: {
-                parent()->removeEventFilter(this);
-                break;
-        }
-        default:
-                break;
-        }
+    switch (event->type()) {
+    case QEvent::ParentChange: {
+        parent()->installEventFilter(this);
+        setGeometry(overlayGeometry());
+        break;
+    }
+    case QEvent::ParentAboutToChange: {
+        parent()->removeEventFilter(this);
+        break;
+    }
+    default:
+        break;
+    }
 
-        return QWidget::event(event);
+    return QWidget::event(event);
 }
 
 bool
 OverlayWidget::eventFilter(QObject *obj, QEvent *event)
 {
-        switch (event->type()) {
-        case QEvent::Move:
-        case QEvent::Resize:
-                setGeometry(overlayGeometry());
-                break;
-        default:
-                break;
-        }
+    switch (event->type()) {
+    case QEvent::Move:
+    case QEvent::Resize:
+        setGeometry(overlayGeometry());
+        break;
+    default:
+        break;
+    }
 
-        return QWidget::eventFilter(obj, event);
+    return QWidget::eventFilter(obj, event);
 }
 
 QRect
 OverlayWidget::overlayGeometry() const
 {
-        QWidget *widget = parentWidget();
+    QWidget *widget = parentWidget();
 
-        if (!widget)
-                return QRect();
+    if (!widget)
+        return QRect();
 
-        return widget->rect();
+    return widget->rect();
 }
 
 void
 OverlayWidget::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event);
+    Q_UNUSED(event);
 
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+    QStyleOption opt;
+    opt.init(this);
+    QPainter p(this);
+    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
 }
diff --git a/src/ui/OverlayWidget.h b/src/ui/OverlayWidget.h
index 05bb86968fdeee642e085550e50174d3abb0fb89..5f023e3564ab56965cbc1ee4a70a577205b663c5 100644
--- a/src/ui/OverlayWidget.h
+++ b/src/ui/OverlayWidget.h
@@ -11,15 +11,15 @@ class QPainter;
 
 class OverlayWidget : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        explicit OverlayWidget(QWidget *parent = nullptr);
+    explicit OverlayWidget(QWidget *parent = nullptr);
 
 protected:
-        bool event(QEvent *event) override;
-        bool eventFilter(QObject *obj, QEvent *event) override;
+    bool event(QEvent *event) override;
+    bool eventFilter(QObject *obj, QEvent *event) override;
 
-        QRect overlayGeometry() const;
-        void paintEvent(QPaintEvent *event) override;
+    QRect overlayGeometry() const;
+    void paintEvent(QPaintEvent *event) override;
 };
diff --git a/src/ui/Painter.h b/src/ui/Painter.h
index 9f97411676c203aca97244a4068b755245198099..f78b55e5c26f8e75debfef1b1f384d8a0d9e7783 100644
--- a/src/ui/Painter.h
+++ b/src/ui/Painter.h
@@ -13,147 +13,141 @@
 class Painter : public QPainter
 {
 public:
-        explicit Painter(QPaintDevice *device)
-          : QPainter(device)
-        {}
-
-        void drawTextLeft(int x, int y, const QString &text)
-        {
-                QFontMetrics m(fontMetrics());
-                drawText(x, y + m.ascent(), text);
-        }
-
-        void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1)
-        {
-                QFontMetrics m(fontMetrics());
-                if (textWidth < 0) {
-                        textWidth = m.horizontalAdvance(text);
-                }
-                drawText((outerw - x - textWidth), y + m.ascent(), text);
-        }
-
-        void drawPixmapLeft(int x, int y, const QPixmap &pix, const QRect &from)
-        {
-                drawPixmap(QPoint(x, y), pix, from);
-        }
-
-        void drawPixmapLeft(const QPoint &p, const QPixmap &pix, const QRect &from)
-        {
-                return drawPixmapLeft(p.x(), p.y(), pix, from);
-        }
-
-        void drawPixmapLeft(int x, int y, int w, int h, const QPixmap &pix, const QRect &from)
-        {
-                drawPixmap(QRect(x, y, w, h), pix, from);
-        }
-
-        void drawPixmapLeft(const QRect &r, const QPixmap &pix, const QRect &from)
-        {
-                return drawPixmapLeft(r.x(), r.y(), r.width(), r.height(), pix, from);
-        }
-
-        void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix)
-        {
-                Q_UNUSED(outerw);
-                drawPixmap(QPoint(x, y), pix);
-        }
-
-        void drawPixmapLeft(const QPoint &p, int outerw, const QPixmap &pix)
-        {
-                return drawPixmapLeft(p.x(), p.y(), outerw, pix);
-        }
-
-        void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix, const QRect &from)
-        {
-                drawPixmap(
-                  QPoint((outerw - x - (from.width() / pix.devicePixelRatio())), y), pix, from);
-        }
-
-        void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix, const QRect &from)
-        {
-                return drawPixmapRight(p.x(), p.y(), outerw, pix, from);
-        }
-        void drawPixmapRight(int x,
-                             int y,
-                             int w,
-                             int h,
-                             int outerw,
-                             const QPixmap &pix,
-                             const QRect &from)
-        {
-                drawPixmap(QRect((outerw - x - w), y, w, h), pix, from);
-        }
-
-        void drawPixmapRight(const QRect &r, int outerw, const QPixmap &pix, const QRect &from)
-        {
-                return drawPixmapRight(r.x(), r.y(), r.width(), r.height(), outerw, pix, from);
-        }
-
-        void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix)
-        {
-                drawPixmap(QPoint((outerw - x - (pix.width() / pix.devicePixelRatio())), y), pix);
-        }
-
-        void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix)
-        {
-                return drawPixmapRight(p.x(), p.y(), outerw, pix);
-        }
-
-        void drawAvatar(const QPixmap &pix, int w, int h, int d)
-        {
-                QPainterPath pp;
-                pp.addEllipse((w - d) / 2, (h - d) / 2, d, d);
-
-                QRect region((w - d) / 2, (h - d) / 2, d, d);
-
-                setClipPath(pp);
-                drawPixmap(region, pix);
-        }
-
-        void drawLetterAvatar(const QString &c,
-                              const QColor &penColor,
-                              const QColor &brushColor,
-                              int w,
-                              int h,
-                              int d)
-        {
-                QRect region((w - d) / 2, (h - d) / 2, d, d);
-
-                setPen(Qt::NoPen);
-                setBrush(brushColor);
-
-                drawEllipse(region.center(), d / 2, d / 2);
-
-                setBrush(Qt::NoBrush);
-                drawEllipse(region.center(), d / 2, d / 2);
-
-                setPen(penColor);
-                drawText(region.translated(0, -1), Qt::AlignCenter, c);
-        }
+    explicit Painter(QPaintDevice *device)
+      : QPainter(device)
+    {}
+
+    void drawTextLeft(int x, int y, const QString &text)
+    {
+        QFontMetrics m(fontMetrics());
+        drawText(x, y + m.ascent(), text);
+    }
+
+    void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1)
+    {
+        QFontMetrics m(fontMetrics());
+        if (textWidth < 0) {
+            textWidth = m.horizontalAdvance(text);
+        }
+        drawText((outerw - x - textWidth), y + m.ascent(), text);
+    }
+
+    void drawPixmapLeft(int x, int y, const QPixmap &pix, const QRect &from)
+    {
+        drawPixmap(QPoint(x, y), pix, from);
+    }
+
+    void drawPixmapLeft(const QPoint &p, const QPixmap &pix, const QRect &from)
+    {
+        return drawPixmapLeft(p.x(), p.y(), pix, from);
+    }
+
+    void drawPixmapLeft(int x, int y, int w, int h, const QPixmap &pix, const QRect &from)
+    {
+        drawPixmap(QRect(x, y, w, h), pix, from);
+    }
+
+    void drawPixmapLeft(const QRect &r, const QPixmap &pix, const QRect &from)
+    {
+        return drawPixmapLeft(r.x(), r.y(), r.width(), r.height(), pix, from);
+    }
+
+    void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix)
+    {
+        Q_UNUSED(outerw);
+        drawPixmap(QPoint(x, y), pix);
+    }
+
+    void drawPixmapLeft(const QPoint &p, int outerw, const QPixmap &pix)
+    {
+        return drawPixmapLeft(p.x(), p.y(), outerw, pix);
+    }
+
+    void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix, const QRect &from)
+    {
+        drawPixmap(QPoint((outerw - x - (from.width() / pix.devicePixelRatio())), y), pix, from);
+    }
+
+    void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix, const QRect &from)
+    {
+        return drawPixmapRight(p.x(), p.y(), outerw, pix, from);
+    }
+    void
+    drawPixmapRight(int x, int y, int w, int h, int outerw, const QPixmap &pix, const QRect &from)
+    {
+        drawPixmap(QRect((outerw - x - w), y, w, h), pix, from);
+    }
+
+    void drawPixmapRight(const QRect &r, int outerw, const QPixmap &pix, const QRect &from)
+    {
+        return drawPixmapRight(r.x(), r.y(), r.width(), r.height(), outerw, pix, from);
+    }
+
+    void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix)
+    {
+        drawPixmap(QPoint((outerw - x - (pix.width() / pix.devicePixelRatio())), y), pix);
+    }
+
+    void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix)
+    {
+        return drawPixmapRight(p.x(), p.y(), outerw, pix);
+    }
+
+    void drawAvatar(const QPixmap &pix, int w, int h, int d)
+    {
+        QPainterPath pp;
+        pp.addEllipse((w - d) / 2, (h - d) / 2, d, d);
+
+        QRect region((w - d) / 2, (h - d) / 2, d, d);
+
+        setClipPath(pp);
+        drawPixmap(region, pix);
+    }
+
+    void drawLetterAvatar(const QString &c,
+                          const QColor &penColor,
+                          const QColor &brushColor,
+                          int w,
+                          int h,
+                          int d)
+    {
+        QRect region((w - d) / 2, (h - d) / 2, d, d);
+
+        setPen(Qt::NoPen);
+        setBrush(brushColor);
+
+        drawEllipse(region.center(), d / 2, d / 2);
+
+        setBrush(Qt::NoBrush);
+        drawEllipse(region.center(), d / 2, d / 2);
+
+        setPen(penColor);
+        drawText(region.translated(0, -1), Qt::AlignCenter, c);
+    }
 };
 
 class PainterHighQualityEnabler
 {
 public:
-        PainterHighQualityEnabler(Painter &p)
-          : _painter(p)
-        {
-                hints_ = QPainter::Antialiasing | QPainter::SmoothPixmapTransform |
-                         QPainter::TextAntialiasing;
+    PainterHighQualityEnabler(Painter &p)
+      : _painter(p)
+    {
+        hints_ =
+          QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing;
 
-                _painter.setRenderHints(hints_);
-        }
+        _painter.setRenderHints(hints_);
+    }
 
-        ~PainterHighQualityEnabler()
-        {
-                if (hints_)
-                        _painter.setRenderHints(hints_, false);
-        }
+    ~PainterHighQualityEnabler()
+    {
+        if (hints_)
+            _painter.setRenderHints(hints_, false);
+    }
 
-        PainterHighQualityEnabler(const PainterHighQualityEnabler &other) = delete;
-        PainterHighQualityEnabler &operator=(const PainterHighQualityEnabler &other) = delete;
+    PainterHighQualityEnabler(const PainterHighQualityEnabler &other) = delete;
+    PainterHighQualityEnabler &operator=(const PainterHighQualityEnabler &other) = delete;
 
 private:
-        Painter &_painter;
-        QPainter::RenderHints hints_ = {};
+    Painter &_painter;
+    QPainter::RenderHints hints_ = {};
 };
diff --git a/src/ui/RaisedButton.cpp b/src/ui/RaisedButton.cpp
index 563cb8e5a6657478a33ac257698236754582e2c2..fd0cbd76b25a6d3d4d55d0e15f8f0eefbbe4d12f 100644
--- a/src/ui/RaisedButton.cpp
+++ b/src/ui/RaisedButton.cpp
@@ -10,68 +10,68 @@
 void
 RaisedButton::init()
 {
-        shadow_state_machine_ = new QStateMachine(this);
-        normal_state_         = new QState;
-        pressed_state_        = new QState;
-        effect_               = new QGraphicsDropShadowEffect;
+    shadow_state_machine_ = new QStateMachine(this);
+    normal_state_         = new QState;
+    pressed_state_        = new QState;
+    effect_               = new QGraphicsDropShadowEffect;
 
-        effect_->setBlurRadius(7);
-        effect_->setOffset(QPointF(0, 2));
-        effect_->setColor(QColor(0, 0, 0, 75));
+    effect_->setBlurRadius(7);
+    effect_->setOffset(QPointF(0, 2));
+    effect_->setColor(QColor(0, 0, 0, 75));
 
-        setBackgroundMode(Qt::OpaqueMode);
-        setMinimumHeight(42);
-        setGraphicsEffect(effect_);
-        setBaseOpacity(0.3);
+    setBackgroundMode(Qt::OpaqueMode);
+    setMinimumHeight(42);
+    setGraphicsEffect(effect_);
+    setBaseOpacity(0.3);
 
-        shadow_state_machine_->addState(normal_state_);
-        shadow_state_machine_->addState(pressed_state_);
+    shadow_state_machine_->addState(normal_state_);
+    shadow_state_machine_->addState(pressed_state_);
 
-        normal_state_->assignProperty(effect_, "offset", QPointF(0, 2));
-        normal_state_->assignProperty(effect_, "blurRadius", 7);
+    normal_state_->assignProperty(effect_, "offset", QPointF(0, 2));
+    normal_state_->assignProperty(effect_, "blurRadius", 7);
 
-        pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5));
-        pressed_state_->assignProperty(effect_, "blurRadius", 29);
+    pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5));
+    pressed_state_->assignProperty(effect_, "blurRadius", 29);
 
-        QAbstractTransition *transition;
+    QAbstractTransition *transition;
 
-        transition = new QEventTransition(this, QEvent::MouseButtonPress);
-        transition->setTargetState(pressed_state_);
-        normal_state_->addTransition(transition);
+    transition = new QEventTransition(this, QEvent::MouseButtonPress);
+    transition->setTargetState(pressed_state_);
+    normal_state_->addTransition(transition);
 
-        transition = new QEventTransition(this, QEvent::MouseButtonDblClick);
-        transition->setTargetState(pressed_state_);
-        normal_state_->addTransition(transition);
+    transition = new QEventTransition(this, QEvent::MouseButtonDblClick);
+    transition->setTargetState(pressed_state_);
+    normal_state_->addTransition(transition);
 
-        transition = new QEventTransition(this, QEvent::MouseButtonRelease);
-        transition->setTargetState(normal_state_);
-        pressed_state_->addTransition(transition);
+    transition = new QEventTransition(this, QEvent::MouseButtonRelease);
+    transition->setTargetState(normal_state_);
+    pressed_state_->addTransition(transition);
 
-        QPropertyAnimation *animation;
+    QPropertyAnimation *animation;
 
-        animation = new QPropertyAnimation(effect_, "offset", this);
-        animation->setDuration(100);
-        shadow_state_machine_->addDefaultAnimation(animation);
+    animation = new QPropertyAnimation(effect_, "offset", this);
+    animation->setDuration(100);
+    shadow_state_machine_->addDefaultAnimation(animation);
 
-        animation = new QPropertyAnimation(effect_, "blurRadius", this);
-        animation->setDuration(100);
-        shadow_state_machine_->addDefaultAnimation(animation);
+    animation = new QPropertyAnimation(effect_, "blurRadius", this);
+    animation->setDuration(100);
+    shadow_state_machine_->addDefaultAnimation(animation);
 
-        shadow_state_machine_->setInitialState(normal_state_);
-        shadow_state_machine_->start();
+    shadow_state_machine_->setInitialState(normal_state_);
+    shadow_state_machine_->start();
 }
 
 RaisedButton::RaisedButton(QWidget *parent)
   : FlatButton(parent)
 {
-        init();
+    init();
 }
 
 RaisedButton::RaisedButton(const QString &text, QWidget *parent)
   : FlatButton(parent)
 {
-        init();
-        setText(text);
+    init();
+    setText(text);
 }
 
 RaisedButton::~RaisedButton() {}
@@ -79,15 +79,15 @@ RaisedButton::~RaisedButton() {}
 bool
 RaisedButton::event(QEvent *event)
 {
-        if (QEvent::EnabledChange == event->type()) {
-                if (isEnabled()) {
-                        shadow_state_machine_->start();
-                        effect_->setEnabled(true);
-                } else {
-                        shadow_state_machine_->stop();
-                        effect_->setEnabled(false);
-                }
+    if (QEvent::EnabledChange == event->type()) {
+        if (isEnabled()) {
+            shadow_state_machine_->start();
+            effect_->setEnabled(true);
+        } else {
+            shadow_state_machine_->stop();
+            effect_->setEnabled(false);
         }
+    }
 
-        return FlatButton::event(event);
+    return FlatButton::event(event);
 }
diff --git a/src/ui/RaisedButton.h b/src/ui/RaisedButton.h
index dcb579bbdffd55813f11d837b6fc4de5ffd37f98..277fa7fff70dc447d21c2412f8ffdc56034b11ff 100644
--- a/src/ui/RaisedButton.h
+++ b/src/ui/RaisedButton.h
@@ -12,21 +12,21 @@
 
 class RaisedButton : public FlatButton
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        explicit RaisedButton(QWidget *parent = nullptr);
-        explicit RaisedButton(const QString &text, QWidget *parent = nullptr);
-        ~RaisedButton() override;
+    explicit RaisedButton(QWidget *parent = nullptr);
+    explicit RaisedButton(const QString &text, QWidget *parent = nullptr);
+    ~RaisedButton() override;
 
 protected:
-        bool event(QEvent *event) override;
+    bool event(QEvent *event) override;
 
 private:
-        void init();
+    void init();
 
-        QStateMachine *shadow_state_machine_;
-        QState *normal_state_;
-        QState *pressed_state_;
-        QGraphicsDropShadowEffect *effect_;
+    QStateMachine *shadow_state_machine_;
+    QState *normal_state_;
+    QState *pressed_state_;
+    QGraphicsDropShadowEffect *effect_;
 };
diff --git a/src/ui/Ripple.cpp b/src/ui/Ripple.cpp
index f0455f0b8fa48f371de718935810e31c8dc56d5a..72cf2da2af18766eb7d8f9c2f5d3b53e009b1999 100644
--- a/src/ui/Ripple.cpp
+++ b/src/ui/Ripple.cpp
@@ -14,7 +14,7 @@ Ripple::Ripple(const QPoint &center, QObject *parent)
   , opacity_(0)
   , center_(center)
 {
-        init();
+    init();
 }
 
 Ripple::Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent)
@@ -26,86 +26,86 @@ Ripple::Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent)
   , opacity_(0)
   , center_(center)
 {
-        init();
+    init();
 }
 
 void
 Ripple::setRadius(qreal radius)
 {
-        Q_ASSERT(overlay_);
+    Q_ASSERT(overlay_);
 
-        if (radius_ == radius)
-                return;
+    if (radius_ == radius)
+        return;
 
-        radius_ = radius;
-        overlay_->update();
+    radius_ = radius;
+    overlay_->update();
 }
 
 void
 Ripple::setOpacity(qreal opacity)
 {
-        Q_ASSERT(overlay_);
+    Q_ASSERT(overlay_);
 
-        if (opacity_ == opacity)
-                return;
+    if (opacity_ == opacity)
+        return;
 
-        opacity_ = opacity;
-        overlay_->update();
+    opacity_ = opacity;
+    overlay_->update();
 }
 
 void
 Ripple::setColor(const QColor &color)
 {
-        if (brush_.color() == color)
-                return;
+    if (brush_.color() == color)
+        return;
 
-        brush_.setColor(color);
+    brush_.setColor(color);
 
-        if (overlay_)
-                overlay_->update();
+    if (overlay_)
+        overlay_->update();
 }
 
 void
 Ripple::setBrush(const QBrush &brush)
 {
-        brush_ = brush;
+    brush_ = brush;
 
-        if (overlay_)
-                overlay_->update();
+    if (overlay_)
+        overlay_->update();
 }
 
 void
 Ripple::destroy()
 {
-        Q_ASSERT(overlay_);
+    Q_ASSERT(overlay_);
 
-        overlay_->removeRipple(this);
+    overlay_->removeRipple(this);
 }
 
 QPropertyAnimation *
 Ripple::animate(const QByteArray &property, const QEasingCurve &easing, int duration)
 {
-        QPropertyAnimation *animation = new QPropertyAnimation;
-        animation->setTargetObject(this);
-        animation->setPropertyName(property);
-        animation->setEasingCurve(easing);
-        animation->setDuration(duration);
+    QPropertyAnimation *animation = new QPropertyAnimation;
+    animation->setTargetObject(this);
+    animation->setPropertyName(property);
+    animation->setEasingCurve(easing);
+    animation->setDuration(duration);
 
-        addAnimation(animation);
+    addAnimation(animation);
 
-        return animation;
+    return animation;
 }
 
 void
 Ripple::init()
 {
-        setOpacityStartValue(0.5);
-        setOpacityEndValue(0);
-        setRadiusStartValue(0);
-        setRadiusEndValue(300);
+    setOpacityStartValue(0.5);
+    setOpacityEndValue(0);
+    setRadiusStartValue(0);
+    setRadiusEndValue(300);
 
-        brush_.setColor(Qt::black);
-        brush_.setStyle(Qt::SolidPattern);
+    brush_.setColor(Qt::black);
+    brush_.setStyle(Qt::SolidPattern);
 
-        connect(this, SIGNAL(finished()), this, SLOT(destroy()));
+    connect(this, SIGNAL(finished()), this, SLOT(destroy()));
 }
diff --git a/src/ui/Ripple.h b/src/ui/Ripple.h
index 2ad42b9ebebcc9654b08e903bc84554ab8f1665a..df09cabaa73aa4974cfa275d13b3a46f22d4de9d 100644
--- a/src/ui/Ripple.h
+++ b/src/ui/Ripple.h
@@ -14,136 +14,136 @@ class RippleOverlay;
 
 class Ripple : public QParallelAnimationGroup
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(qreal radius WRITE setRadius READ radius)
-        Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity)
+    Q_PROPERTY(qreal radius WRITE setRadius READ radius)
+    Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity)
 
 public:
-        explicit Ripple(const QPoint &center, QObject *parent = nullptr);
-        Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent = nullptr);
+    explicit Ripple(const QPoint &center, QObject *parent = nullptr);
+    Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent = nullptr);
 
-        inline void setOverlay(RippleOverlay *overlay);
+    inline void setOverlay(RippleOverlay *overlay);
 
-        void setRadius(qreal radius);
-        void setOpacity(qreal opacity);
-        void setColor(const QColor &color);
-        void setBrush(const QBrush &brush);
+    void setRadius(qreal radius);
+    void setOpacity(qreal opacity);
+    void setColor(const QColor &color);
+    void setBrush(const QBrush &brush);
 
-        inline qreal radius() const;
-        inline qreal opacity() const;
-        inline QColor color() const;
-        inline QBrush brush() const;
-        inline QPoint center() const;
+    inline qreal radius() const;
+    inline qreal opacity() const;
+    inline QColor color() const;
+    inline QBrush brush() const;
+    inline QPoint center() const;
 
-        inline QPropertyAnimation *radiusAnimation() const;
-        inline QPropertyAnimation *opacityAnimation() const;
+    inline QPropertyAnimation *radiusAnimation() const;
+    inline QPropertyAnimation *opacityAnimation() const;
 
-        inline void setOpacityStartValue(qreal value);
-        inline void setOpacityEndValue(qreal value);
-        inline void setRadiusStartValue(qreal value);
-        inline void setRadiusEndValue(qreal value);
-        inline void setDuration(int msecs);
+    inline void setOpacityStartValue(qreal value);
+    inline void setOpacityEndValue(qreal value);
+    inline void setRadiusStartValue(qreal value);
+    inline void setRadiusEndValue(qreal value);
+    inline void setDuration(int msecs);
 
 protected slots:
-        void destroy();
+    void destroy();
 
 private:
-        Q_DISABLE_COPY(Ripple)
+    Q_DISABLE_COPY(Ripple)
 
-        QPropertyAnimation *animate(const QByteArray &property,
-                                    const QEasingCurve &easing = QEasingCurve::OutQuad,
-                                    int duration               = 800);
+    QPropertyAnimation *animate(const QByteArray &property,
+                                const QEasingCurve &easing = QEasingCurve::OutQuad,
+                                int duration               = 800);
 
-        void init();
+    void init();
 
-        RippleOverlay *overlay_;
+    RippleOverlay *overlay_;
 
-        QPropertyAnimation *const radius_anim_;
-        QPropertyAnimation *const opacity_anim_;
+    QPropertyAnimation *const radius_anim_;
+    QPropertyAnimation *const opacity_anim_;
 
-        qreal radius_;
-        qreal opacity_;
+    qreal radius_;
+    qreal opacity_;
 
-        QPoint center_;
-        QBrush brush_;
+    QPoint center_;
+    QBrush brush_;
 };
 
 inline void
 Ripple::setOverlay(RippleOverlay *overlay)
 {
-        overlay_ = overlay;
+    overlay_ = overlay;
 }
 
 inline qreal
 Ripple::radius() const
 {
-        return radius_;
+    return radius_;
 }
 
 inline qreal
 Ripple::opacity() const
 {
-        return opacity_;
+    return opacity_;
 }
 
 inline QColor
 Ripple::color() const
 {
-        return brush_.color();
+    return brush_.color();
 }
 
 inline QBrush
 Ripple::brush() const
 {
-        return brush_;
+    return brush_;
 }
 
 inline QPoint
 Ripple::center() const
 {
-        return center_;
+    return center_;
 }
 
 inline QPropertyAnimation *
 Ripple::radiusAnimation() const
 {
-        return radius_anim_;
+    return radius_anim_;
 }
 
 inline QPropertyAnimation *
 Ripple::opacityAnimation() const
 {
-        return opacity_anim_;
+    return opacity_anim_;
 }
 
 inline void
 Ripple::setOpacityStartValue(qreal value)
 {
-        opacity_anim_->setStartValue(value);
+    opacity_anim_->setStartValue(value);
 }
 
 inline void
 Ripple::setOpacityEndValue(qreal value)
 {
-        opacity_anim_->setEndValue(value);
+    opacity_anim_->setEndValue(value);
 }
 
 inline void
 Ripple::setRadiusStartValue(qreal value)
 {
-        radius_anim_->setStartValue(value);
+    radius_anim_->setStartValue(value);
 }
 
 inline void
 Ripple::setRadiusEndValue(qreal value)
 {
-        radius_anim_->setEndValue(value);
+    radius_anim_->setEndValue(value);
 }
 
 inline void
 Ripple::setDuration(int msecs)
 {
-        radius_anim_->setDuration(msecs);
-        opacity_anim_->setDuration(msecs);
+    radius_anim_->setDuration(msecs);
+    opacity_anim_->setDuration(msecs);
 }
diff --git a/src/ui/RippleOverlay.cpp b/src/ui/RippleOverlay.cpp
index 00915deb844392720c3b5d142739915726dd8066..6745cc377ab4473ab2e72cda2850c89e9a75aa1f 100644
--- a/src/ui/RippleOverlay.cpp
+++ b/src/ui/RippleOverlay.cpp
@@ -11,56 +11,56 @@ RippleOverlay::RippleOverlay(QWidget *parent)
   : OverlayWidget(parent)
   , use_clip_(false)
 {
-        setAttribute(Qt::WA_TransparentForMouseEvents);
-        setAttribute(Qt::WA_NoSystemBackground);
+    setAttribute(Qt::WA_TransparentForMouseEvents);
+    setAttribute(Qt::WA_NoSystemBackground);
 }
 
 void
 RippleOverlay::addRipple(Ripple *ripple)
 {
-        ripple->setOverlay(this);
-        ripples_.push_back(ripple);
-        ripple->start();
+    ripple->setOverlay(this);
+    ripples_.push_back(ripple);
+    ripple->start();
 }
 
 void
 RippleOverlay::addRipple(const QPoint &position, qreal radius)
 {
-        Ripple *ripple = new Ripple(position);
-        ripple->setRadiusEndValue(radius);
-        addRipple(ripple);
+    Ripple *ripple = new Ripple(position);
+    ripple->setRadiusEndValue(radius);
+    addRipple(ripple);
 }
 
 void
 RippleOverlay::removeRipple(Ripple *ripple)
 {
-        if (ripples_.removeOne(ripple))
-                delete ripple;
+    if (ripples_.removeOne(ripple))
+        delete ripple;
 }
 
 void
 RippleOverlay::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event)
+    Q_UNUSED(event)
 
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-        painter.setPen(Qt::NoPen);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
+    painter.setPen(Qt::NoPen);
 
-        if (use_clip_)
-                painter.setClipPath(clip_path_);
+    if (use_clip_)
+        painter.setClipPath(clip_path_);
 
-        for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); ++it)
-                paintRipple(&painter, *it);
+    for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); ++it)
+        paintRipple(&painter, *it);
 }
 
 void
 RippleOverlay::paintRipple(QPainter *painter, Ripple *ripple)
 {
-        const qreal radius   = ripple->radius();
-        const QPointF center = ripple->center();
+    const qreal radius   = ripple->radius();
+    const QPointF center = ripple->center();
 
-        painter->setOpacity(ripple->opacity());
-        painter->setBrush(ripple->brush());
-        painter->drawEllipse(center, radius, radius);
+    painter->setOpacity(ripple->opacity());
+    painter->setBrush(ripple->brush());
+    painter->drawEllipse(center, radius, radius);
 }
diff --git a/src/ui/RippleOverlay.h b/src/ui/RippleOverlay.h
index 7ae3e4f10a541d2d57d507ce2634f287a72b942d..3256c28ddb0c0c3c92e48cb3ecf77f9b00ea1c5f 100644
--- a/src/ui/RippleOverlay.h
+++ b/src/ui/RippleOverlay.h
@@ -12,50 +12,50 @@ class Ripple;
 
 class RippleOverlay : public OverlayWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        explicit RippleOverlay(QWidget *parent = nullptr);
+    explicit RippleOverlay(QWidget *parent = nullptr);
 
-        void addRipple(Ripple *ripple);
-        void addRipple(const QPoint &position, qreal radius = 300);
+    void addRipple(Ripple *ripple);
+    void addRipple(const QPoint &position, qreal radius = 300);
 
-        void removeRipple(Ripple *ripple);
+    void removeRipple(Ripple *ripple);
 
-        inline void setClipping(bool enable);
-        inline bool hasClipping() const;
+    inline void setClipping(bool enable);
+    inline bool hasClipping() const;
 
-        inline void setClipPath(const QPainterPath &path);
+    inline void setClipPath(const QPainterPath &path);
 
 protected:
-        void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
+    void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
 
 private:
-        Q_DISABLE_COPY(RippleOverlay)
+    Q_DISABLE_COPY(RippleOverlay)
 
-        void paintRipple(QPainter *painter, Ripple *ripple);
+    void paintRipple(QPainter *painter, Ripple *ripple);
 
-        QList<Ripple *> ripples_;
-        QPainterPath clip_path_;
-        bool use_clip_;
+    QList<Ripple *> ripples_;
+    QPainterPath clip_path_;
+    bool use_clip_;
 };
 
 inline void
 RippleOverlay::setClipping(bool enable)
 {
-        use_clip_ = enable;
-        update();
+    use_clip_ = enable;
+    update();
 }
 
 inline bool
 RippleOverlay::hasClipping() const
 {
-        return use_clip_;
+    return use_clip_;
 }
 
 inline void
 RippleOverlay::setClipPath(const QPainterPath &path)
 {
-        clip_path_ = path;
-        update();
+    clip_path_ = path;
+    update();
 }
diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp
index fcba82056da1a14bdf1f941c55103a69b95b288c..ed8a7cd8407af0151acd883ee2e87383a7c6081a 100644
--- a/src/ui/RoomSettings.cpp
+++ b/src/ui/RoomSettings.cpp
@@ -27,451 +27,473 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
   : QWidget(parent)
   , roomId_{roomId}
 {
-        setAutoFillBackground(true);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-
-        QFont largeFont;
-        largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4);
-        setMinimumWidth(conf::window::minModalWidth);
-        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
-        auto layout = new QVBoxLayout(this);
-
-        applyBtn_  = new QPushButton(tr("Apply"), this);
-        cancelBtn_ = new QPushButton(tr("Cancel"), this);
-        cancelBtn_->setDefault(true);
-
-        auto btnLayout = new QHBoxLayout;
-        btnLayout->addStretch(1);
-        btnLayout->setSpacing(15);
-        btnLayout->addWidget(cancelBtn_);
-        btnLayout->addWidget(applyBtn_);
-
-        nameInput_ = new TextField(this);
-        nameInput_->setLabel(tr("Name").toUpper());
-        topicInput_ = new TextField(this);
-        topicInput_->setLabel(tr("Topic").toUpper());
-
-        errorField_ = new QLabel(this);
-        errorField_->setWordWrap(true);
-        errorField_->hide();
-
-        layout->addWidget(nameInput_);
-        layout->addWidget(topicInput_);
-        layout->addLayout(btnLayout, 1);
-
-        auto labelLayout = new QHBoxLayout;
-        labelLayout->setAlignment(Qt::AlignHCenter);
-        labelLayout->addWidget(errorField_);
-        layout->addLayout(labelLayout);
-
-        connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked);
-        connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
-
-        auto window = QApplication::activeWindow();
-
-        if (window != nullptr) {
-                auto center = window->frameGeometry().center();
-                move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
-        }
+    setAutoFillBackground(true);
+    setAttribute(Qt::WA_DeleteOnClose, true);
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+    setWindowModality(Qt::WindowModal);
+
+    QFont largeFont;
+    largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4);
+    setMinimumWidth(conf::window::minModalWidth);
+    setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+    auto layout = new QVBoxLayout(this);
+
+    applyBtn_  = new QPushButton(tr("Apply"), this);
+    cancelBtn_ = new QPushButton(tr("Cancel"), this);
+    cancelBtn_->setDefault(true);
+
+    auto btnLayout = new QHBoxLayout;
+    btnLayout->addStretch(1);
+    btnLayout->setSpacing(15);
+    btnLayout->addWidget(cancelBtn_);
+    btnLayout->addWidget(applyBtn_);
+
+    nameInput_ = new TextField(this);
+    nameInput_->setLabel(tr("Name").toUpper());
+    topicInput_ = new TextField(this);
+    topicInput_->setLabel(tr("Topic").toUpper());
+
+    errorField_ = new QLabel(this);
+    errorField_->setWordWrap(true);
+    errorField_->hide();
+
+    layout->addWidget(nameInput_);
+    layout->addWidget(topicInput_);
+    layout->addLayout(btnLayout, 1);
+
+    auto labelLayout = new QHBoxLayout;
+    labelLayout->setAlignment(Qt::AlignHCenter);
+    labelLayout->addWidget(errorField_);
+    layout->addLayout(labelLayout);
+
+    connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked);
+    connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
+
+    auto window = QApplication::activeWindow();
+
+    if (window != nullptr) {
+        auto center = window->frameGeometry().center();
+        move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
+    }
 }
 
 void
 EditModal::topicEventSent(const QString &topic)
 {
-        errorField_->hide();
-        emit topicChanged(topic);
-        close();
+    errorField_->hide();
+    emit topicChanged(topic);
+    close();
 }
 
 void
 EditModal::nameEventSent(const QString &name)
 {
-        errorField_->hide();
-        emit nameChanged(name);
-        close();
+    errorField_->hide();
+    emit nameChanged(name);
+    close();
 }
 
 void
 EditModal::error(const QString &msg)
 {
-        errorField_->setText(msg);
-        errorField_->show();
+    errorField_->setText(msg);
+    errorField_->show();
 }
 
 void
 EditModal::applyClicked()
 {
-        // Check if the values are changed from the originals.
-        auto newName  = nameInput_->text().trimmed();
-        auto newTopic = topicInput_->text().trimmed();
+    // Check if the values are changed from the originals.
+    auto newName  = nameInput_->text().trimmed();
+    auto newTopic = topicInput_->text().trimmed();
 
-        errorField_->hide();
+    errorField_->hide();
 
-        if (newName == initialName_ && newTopic == initialTopic_) {
-                close();
-                return;
-        }
+    if (newName == initialName_ && newTopic == initialTopic_) {
+        close();
+        return;
+    }
 
-        using namespace mtx::events;
-        auto proxy = std::make_shared<ThreadProxy>();
-        connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent);
-        connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent);
-        connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error);
-
-        if (newName != initialName_ && !newName.isEmpty()) {
-                state::Name body;
-                body.name = newName.toStdString();
-
-                http::client()->send_state_event(
-                  roomId_.toStdString(),
-                  body,
-                  [proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) {
-                          if (err) {
-                                  emit proxy->error(
-                                    QString::fromStdString(err->matrix_error.error));
-                                  return;
-                          }
-
-                          emit proxy->nameEventSent(newName);
-                  });
-        }
+    using namespace mtx::events;
+    auto proxy = std::make_shared<ThreadProxy>();
+    connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent);
+    connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent);
+    connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error);
 
-        if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
-                state::Topic body;
-                body.topic = newTopic.toStdString();
-
-                http::client()->send_state_event(
-                  roomId_.toStdString(),
-                  body,
-                  [proxy, newTopic](const mtx::responses::EventId &, mtx::http::RequestErr err) {
-                          if (err) {
-                                  emit proxy->error(
-                                    QString::fromStdString(err->matrix_error.error));
-                                  return;
-                          }
-
-                          emit proxy->topicEventSent(newTopic);
-                  });
-        }
+    if (newName != initialName_ && !newName.isEmpty()) {
+        state::Name body;
+        body.name = newName.toStdString();
+
+        http::client()->send_state_event(
+          roomId_.toStdString(),
+          body,
+          [proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+              if (err) {
+                  emit proxy->error(QString::fromStdString(err->matrix_error.error));
+                  return;
+              }
+
+              emit proxy->nameEventSent(newName);
+          });
+    }
+
+    if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
+        state::Topic body;
+        body.topic = newTopic.toStdString();
+
+        http::client()->send_state_event(
+          roomId_.toStdString(),
+          body,
+          [proxy, newTopic](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+              if (err) {
+                  emit proxy->error(QString::fromStdString(err->matrix_error.error));
+                  return;
+              }
+
+              emit proxy->topicEventSent(newTopic);
+          });
+    }
 }
 
 void
 EditModal::setFields(const QString &roomName, const QString &roomTopic)
 {
-        initialName_  = roomName;
-        initialTopic_ = roomTopic;
+    initialName_  = roomName;
+    initialTopic_ = roomTopic;
 
-        nameInput_->setText(roomName);
-        topicInput_->setText(roomTopic);
+    nameInput_->setText(roomName);
+    topicInput_->setText(roomTopic);
 }
 
 RoomSettings::RoomSettings(QString roomid, QObject *parent)
   : QObject(parent)
   , roomid_{std::move(roomid)}
 {
-        retrieveRoomInfo();
-
-        // get room setting notifications
-        http::client()->get_pushrules(
-          "global",
-          "override",
-          roomid_.toStdString(),
-          [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) {
-                  if (err) {
-                          if (err->status_code == 404)
-                                  http::client()->get_pushrules(
-                                    "global",
-                                    "room",
-                                    roomid_.toStdString(),
-                                    [this](const mtx::pushrules::PushRule &rule,
-                                           mtx::http::RequestErr &err) {
-                                            if (err) {
-                                                    notifications_ = 2; // all messages
-                                                    emit notificationsChanged();
-                                                    return;
-                                            }
-
-                                            if (rule.enabled) {
-                                                    notifications_ = 1; // mentions only
-                                                    emit notificationsChanged();
-                                            }
-                                    });
-                          return;
-                  }
-
-                  if (rule.enabled) {
-                          notifications_ = 0; // muted
-                          emit notificationsChanged();
-                  } else {
-                          notifications_ = 2; // all messages
-                          emit notificationsChanged();
-                  }
-          });
-
-        // access rules
-        if (info_.join_rule == state::JoinRule::Public) {
-                if (info_.guest_access) {
-                        accessRules_ = 0;
-                } else {
-                        accessRules_ = 1;
-                }
+    retrieveRoomInfo();
+
+    // get room setting notifications
+    http::client()->get_pushrules(
+      "global",
+      "override",
+      roomid_.toStdString(),
+      [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) {
+          if (err) {
+              if (err->status_code == 404)
+                  http::client()->get_pushrules(
+                    "global",
+                    "room",
+                    roomid_.toStdString(),
+                    [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) {
+                        if (err) {
+                            notifications_ = 2; // all messages
+                            emit notificationsChanged();
+                            return;
+                        }
+
+                        if (rule.enabled) {
+                            notifications_ = 1; // mentions only
+                            emit notificationsChanged();
+                        }
+                    });
+              return;
+          }
+
+          if (rule.enabled) {
+              notifications_ = 0; // muted
+              emit notificationsChanged();
+          } else {
+              notifications_ = 2; // all messages
+              emit notificationsChanged();
+          }
+      });
+
+    // access rules
+    if (info_.join_rule == state::JoinRule::Public) {
+        if (info_.guest_access) {
+            accessRules_ = 0;
         } else {
-                accessRules_ = 2;
+            accessRules_ = 1;
         }
-        emit accessJoinRulesChanged();
+    } else if (info_.join_rule == state::JoinRule::Invite) {
+        accessRules_ = 2;
+    } else if (info_.join_rule == state::JoinRule::Knock) {
+        accessRules_ = 3;
+    } else if (info_.join_rule == state::JoinRule::Restricted) {
+        accessRules_ = 4;
+    }
+    emit accessJoinRulesChanged();
 }
 
 QString
 RoomSettings::roomName() const
 {
-        return utils::replaceEmoji(QString::fromStdString(info_.name).toHtmlEscaped());
+    return utils::replaceEmoji(QString::fromStdString(info_.name).toHtmlEscaped());
 }
 
 QString
 RoomSettings::roomTopic() const
 {
-        return utils::replaceEmoji(utils::linkifyMessage(
-          QString::fromStdString(info_.topic).toHtmlEscaped().replace("\n", "<br>")));
+    return utils::replaceEmoji(utils::linkifyMessage(
+      QString::fromStdString(info_.topic).toHtmlEscaped().replace("\n", "<br>")));
 }
 
 QString
 RoomSettings::roomId() const
 {
-        return roomid_;
+    return roomid_;
 }
 
 QString
 RoomSettings::roomVersion() const
 {
-        return QString::fromStdString(info_.version);
+    return QString::fromStdString(info_.version);
 }
 
 bool
 RoomSettings::isLoading() const
 {
-        return isLoading_;
+    return isLoading_;
 }
 
 QString
 RoomSettings::roomAvatarUrl()
 {
-        return QString::fromStdString(info_.avatar_url);
+    return QString::fromStdString(info_.avatar_url);
 }
 
 int
 RoomSettings::memberCount() const
 {
-        return info_.member_count;
+    return info_.member_count;
 }
 
 void
 RoomSettings::retrieveRoomInfo()
 {
-        try {
-                usesEncryption_ = cache::isRoomEncrypted(roomid_.toStdString());
-                info_           = cache::singleRoomInfo(roomid_.toStdString());
-        } catch (const lmdb::error &) {
-                nhlog::db()->warn("failed to retrieve room info from cache: {}",
-                                  roomid_.toStdString());
-        }
+    try {
+        usesEncryption_ = cache::isRoomEncrypted(roomid_.toStdString());
+        info_           = cache::singleRoomInfo(roomid_.toStdString());
+    } catch (const lmdb::error &) {
+        nhlog::db()->warn("failed to retrieve room info from cache: {}", roomid_.toStdString());
+    }
 }
 
 int
 RoomSettings::notifications()
 {
-        return notifications_;
+    return notifications_;
 }
 
 int
 RoomSettings::accessJoinRules()
 {
-        return accessRules_;
+    return accessRules_;
 }
 
 void
 RoomSettings::enableEncryption()
 {
-        if (usesEncryption_)
-                return;
-
-        const auto room_id = roomid_.toStdString();
-        http::client()->enable_encryption(
-          room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
-                  if (err) {
-                          int status_code = static_cast<int>(err->status_code);
-                          nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
-                                             room_id,
-                                             err->matrix_error.error,
-                                             status_code);
-                          emit displayError(
-                            tr("Failed to enable encryption: %1")
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                          usesEncryption_ = false;
-                          emit encryptionChanged();
-                          return;
-                  }
-
-                  nhlog::net()->info("enabled encryption on room ({})", room_id);
-          });
-
-        usesEncryption_ = true;
-        emit encryptionChanged();
+    if (usesEncryption_)
+        return;
+
+    const auto room_id = roomid_.toStdString();
+    http::client()->enable_encryption(
+      room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+          if (err) {
+              int status_code = static_cast<int>(err->status_code);
+              nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
+                                 room_id,
+                                 err->matrix_error.error,
+                                 status_code);
+              emit displayError(tr("Failed to enable encryption: %1")
+                                  .arg(QString::fromStdString(err->matrix_error.error)));
+              usesEncryption_ = false;
+              emit encryptionChanged();
+              return;
+          }
+
+          nhlog::net()->info("enabled encryption on room ({})", room_id);
+      });
+
+    usesEncryption_ = true;
+    emit encryptionChanged();
 }
 
 bool
 RoomSettings::canChangeJoinRules() const
 {
-        try {
-                return cache::hasEnoughPowerLevel({EventType::RoomJoinRules},
-                                                  roomid_.toStdString(),
-                                                  utils::localUser().toStdString());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn("lmdb error: {}", e.what());
-        }
-
-        return false;
+    try {
+        return cache::hasEnoughPowerLevel(
+          {EventType::RoomJoinRules}, roomid_.toStdString(), utils::localUser().toStdString());
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("lmdb error: {}", e.what());
+    }
+
+    return false;
 }
 
 bool
 RoomSettings::canChangeNameAndTopic() const
 {
-        try {
-                return cache::hasEnoughPowerLevel({EventType::RoomName, EventType::RoomTopic},
-                                                  roomid_.toStdString(),
-                                                  utils::localUser().toStdString());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn("lmdb error: {}", e.what());
-        }
-
-        return false;
+    try {
+        return cache::hasEnoughPowerLevel({EventType::RoomName, EventType::RoomTopic},
+                                          roomid_.toStdString(),
+                                          utils::localUser().toStdString());
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("lmdb error: {}", e.what());
+    }
+
+    return false;
 }
 
 bool
 RoomSettings::canChangeAvatar() const
 {
-        try {
-                return cache::hasEnoughPowerLevel(
-                  {EventType::RoomAvatar}, roomid_.toStdString(), utils::localUser().toStdString());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn("lmdb error: {}", e.what());
-        }
-
-        return false;
+    try {
+        return cache::hasEnoughPowerLevel(
+          {EventType::RoomAvatar}, roomid_.toStdString(), utils::localUser().toStdString());
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("lmdb error: {}", e.what());
+    }
+
+    return false;
 }
 
 bool
 RoomSettings::isEncryptionEnabled() const
 {
-        return usesEncryption_;
+    return usesEncryption_;
+}
+
+bool
+RoomSettings::supportsKnocking() const
+{
+    return info_.version != "" && info_.version != "1" && info_.version != "2" &&
+           info_.version != "3" && info_.version != "4" && info_.version != "5" &&
+           info_.version != "6";
+}
+bool
+RoomSettings::supportsRestricted() const
+{
+    return info_.version != "" && info_.version != "1" && info_.version != "2" &&
+           info_.version != "3" && info_.version != "4" && info_.version != "5" &&
+           info_.version != "6" && info_.version != "7";
 }
 
 void
 RoomSettings::openEditModal()
 {
-        retrieveRoomInfo();
-
-        auto modal = new EditModal(roomid_);
-        modal->setFields(QString::fromStdString(info_.name), QString::fromStdString(info_.topic));
-        modal->raise();
-        modal->show();
-        connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) {
-                info_.name = newName.toStdString();
-                emit roomNameChanged();
-        });
-
-        connect(modal, &EditModal::topicChanged, this, [this](const QString &newTopic) {
-                info_.topic = newTopic.toStdString();
-                emit roomTopicChanged();
-        });
+    retrieveRoomInfo();
+
+    auto modal = new EditModal(roomid_);
+    modal->setFields(QString::fromStdString(info_.name), QString::fromStdString(info_.topic));
+    modal->raise();
+    modal->show();
+    connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) {
+        info_.name = newName.toStdString();
+        emit roomNameChanged();
+    });
+
+    connect(modal, &EditModal::topicChanged, this, [this](const QString &newTopic) {
+        info_.topic = newTopic.toStdString();
+        emit roomTopicChanged();
+    });
 }
 
 void
 RoomSettings::changeNotifications(int currentIndex)
 {
-        notifications_ = currentIndex;
-
-        std::string room_id = roomid_.toStdString();
-        if (notifications_ == 0) {
-                // mute room
-                // delete old rule first, then add new rule
-                mtx::pushrules::PushRule rule;
-                rule.actions = {mtx::pushrules::actions::dont_notify{}};
-                mtx::pushrules::PushCondition condition;
-                condition.kind    = "event_match";
-                condition.key     = "room_id";
-                condition.pattern = room_id;
-                rule.conditions   = {condition};
-
-                http::client()->put_pushrules(
-                  "global", "override", room_id, rule, [room_id](mtx::http::RequestErr &err) {
-                          if (err)
-                                  nhlog::net()->error("failed to set pushrule for room {}: {} {}",
-                                                      room_id,
-                                                      static_cast<int>(err->status_code),
-                                                      err->matrix_error.error);
-                          http::client()->delete_pushrules(
-                            "global", "room", room_id, [room_id](mtx::http::RequestErr &) {});
-                  });
-        } else if (notifications_ == 1) {
-                // mentions only
-                // delete old rule first, then add new rule
-                mtx::pushrules::PushRule rule;
-                rule.actions = {mtx::pushrules::actions::dont_notify{}};
-                http::client()->put_pushrules(
-                  "global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) {
-                          if (err)
-                                  nhlog::net()->error("failed to set pushrule for room {}: {} {}",
-                                                      room_id,
-                                                      static_cast<int>(err->status_code),
-                                                      err->matrix_error.error);
-                          http::client()->delete_pushrules(
-                            "global", "override", room_id, [room_id](mtx::http::RequestErr &) {});
-                  });
-        } else {
-                // all messages
-                http::client()->delete_pushrules(
-                  "global", "override", room_id, [room_id](mtx::http::RequestErr &) {
-                          http::client()->delete_pushrules(
-                            "global", "room", room_id, [room_id](mtx::http::RequestErr &) {});
-                  });
-        }
+    notifications_ = currentIndex;
+
+    std::string room_id = roomid_.toStdString();
+    if (notifications_ == 0) {
+        // mute room
+        // delete old rule first, then add new rule
+        mtx::pushrules::PushRule rule;
+        rule.actions = {mtx::pushrules::actions::dont_notify{}};
+        mtx::pushrules::PushCondition condition;
+        condition.kind    = "event_match";
+        condition.key     = "room_id";
+        condition.pattern = room_id;
+        rule.conditions   = {condition};
+
+        http::client()->put_pushrules(
+          "global", "override", room_id, rule, [room_id](mtx::http::RequestErr &err) {
+              if (err)
+                  nhlog::net()->error("failed to set pushrule for room {}: {} {}",
+                                      room_id,
+                                      static_cast<int>(err->status_code),
+                                      err->matrix_error.error);
+              http::client()->delete_pushrules(
+                "global", "room", room_id, [room_id](mtx::http::RequestErr &) {});
+          });
+    } else if (notifications_ == 1) {
+        // mentions only
+        // delete old rule first, then add new rule
+        mtx::pushrules::PushRule rule;
+        rule.actions = {mtx::pushrules::actions::dont_notify{}};
+        http::client()->put_pushrules(
+          "global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) {
+              if (err)
+                  nhlog::net()->error("failed to set pushrule for room {}: {} {}",
+                                      room_id,
+                                      static_cast<int>(err->status_code),
+                                      err->matrix_error.error);
+              http::client()->delete_pushrules(
+                "global", "override", room_id, [room_id](mtx::http::RequestErr &) {});
+          });
+    } else {
+        // all messages
+        http::client()->delete_pushrules(
+          "global", "override", room_id, [room_id](mtx::http::RequestErr &) {
+              http::client()->delete_pushrules(
+                "global", "room", room_id, [room_id](mtx::http::RequestErr &) {});
+          });
+    }
 }
 
 void
 RoomSettings::changeAccessRules(int index)
 {
-        using namespace mtx::events::state;
-
-        auto guest_access = [](int index) -> state::GuestAccess {
-                state::GuestAccess event;
-
-                if (index == 0)
-                        event.guest_access = state::AccessState::CanJoin;
-                else
-                        event.guest_access = state::AccessState::Forbidden;
-
-                return event;
-        }(index);
-
-        auto join_rule = [](int index) -> state::JoinRules {
-                state::JoinRules event;
-
-                switch (index) {
-                case 0:
-                case 1:
-                        event.join_rule = state::JoinRule::Public;
-                        break;
-                default:
-                        event.join_rule = state::JoinRule::Invite;
-                }
+    using namespace mtx::events::state;
+
+    auto guest_access = [](int index) -> state::GuestAccess {
+        state::GuestAccess event;
+
+        if (index == 0)
+            event.guest_access = state::AccessState::CanJoin;
+        else
+            event.guest_access = state::AccessState::Forbidden;
+
+        return event;
+    }(index);
+
+    auto join_rule = [](int index) -> state::JoinRules {
+        state::JoinRules event;
+
+        switch (index) {
+        case 0:
+        case 1:
+            event.join_rule = state::JoinRule::Public;
+            break;
+        case 2:
+            event.join_rule = state::JoinRule::Invite;
+            break;
+        case 3:
+            event.join_rule = state::JoinRule::Knock;
+            break;
+        case 4:
+            event.join_rule = state::JoinRule::Restricted;
+            break;
+        default:
+            event.join_rule = state::JoinRule::Invite;
+        }
 
-                return event;
-        }(index);
+        return event;
+    }(index);
 
-        updateAccessRules(roomid_.toStdString(), join_rule, guest_access);
+    updateAccessRules(roomid_.toStdString(), join_rule, guest_access);
 }
 
 void
@@ -479,139 +501,134 @@ RoomSettings::updateAccessRules(const std::string &room_id,
                                 const mtx::events::state::JoinRules &join_rule,
                                 const mtx::events::state::GuestAccess &guest_access)
 {
-        isLoading_ = true;
-        emit loadingChanged();
+    isLoading_ = true;
+    emit loadingChanged();
+
+    http::client()->send_state_event(
+      room_id,
+      join_rule,
+      [this, room_id, guest_access](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to send m.room.join_rule: {} {}",
+                                 static_cast<int>(err->status_code),
+                                 err->matrix_error.error);
+              emit displayError(QString::fromStdString(err->matrix_error.error));
+              isLoading_ = false;
+              emit loadingChanged();
+              return;
+          }
+
+          http::client()->send_state_event(
+            room_id,
+            guest_access,
+            [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+                if (err) {
+                    nhlog::net()->warn("failed to send m.room.guest_access: {} {}",
+                                       static_cast<int>(err->status_code),
+                                       err->matrix_error.error);
+                    emit displayError(QString::fromStdString(err->matrix_error.error));
+                }
 
-        http::client()->send_state_event(
-          room_id,
-          join_rule,
-          [this, room_id, guest_access](const mtx::responses::EventId &,
-                                        mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to send m.room.join_rule: {} {}",
-                                             static_cast<int>(err->status_code),
-                                             err->matrix_error.error);
-                          emit displayError(QString::fromStdString(err->matrix_error.error));
-                          isLoading_ = false;
-                          emit loadingChanged();
-                          return;
-                  }
-
-                  http::client()->send_state_event(
-                    room_id,
-                    guest_access,
-                    [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
-                            if (err) {
-                                    nhlog::net()->warn("failed to send m.room.guest_access: {} {}",
-                                                       static_cast<int>(err->status_code),
-                                                       err->matrix_error.error);
-                                    emit displayError(
-                                      QString::fromStdString(err->matrix_error.error));
-                            }
-
-                            isLoading_ = false;
-                            emit loadingChanged();
-                    });
-          });
+                isLoading_ = false;
+                emit loadingChanged();
+            });
+      });
 }
 
 void
 RoomSettings::stopLoading()
 {
-        isLoading_ = false;
-        emit loadingChanged();
+    isLoading_ = false;
+    emit loadingChanged();
 }
 
 void
 RoomSettings::avatarChanged()
 {
-        retrieveRoomInfo();
-        emit avatarUrlChanged();
+    retrieveRoomInfo();
+    emit avatarUrlChanged();
 }
 
 void
 RoomSettings::updateAvatar()
 {
-        const QString picturesFolder =
-          QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
-        const QString fileName = QFileDialog::getOpenFileName(
-          nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
-
-        if (fileName.isEmpty())
-                return;
-
-        QMimeDatabase db;
-        QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
-
-        const auto format = mime.name().split("/")[0];
-
-        QFile file{fileName, this};
-        if (format != "image") {
-                emit displayError(tr("The selected file is not an image"));
-                return;
-        }
-
-        if (!file.open(QIODevice::ReadOnly)) {
-                emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
-                return;
-        }
-
-        isLoading_ = true;
-        emit loadingChanged();
-
-        // Events emitted from the http callbacks (different threads) will
-        // be queued back into the UI thread through this proxy object.
-        auto proxy = std::make_shared<ThreadProxy>();
-        connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError);
-        connect(proxy.get(), &ThreadProxy::stopLoading, this, &RoomSettings::stopLoading);
-
-        const auto bin        = file.peek(file.size());
-        const auto payload    = std::string(bin.data(), bin.size());
-        const auto dimensions = QImageReader(&file).size();
-
-        // First we need to create a new mxc URI
-        // (i.e upload media to the Matrix content repository) for the new avatar.
-        http::client()->upload(
-          payload,
-          mime.name().toStdString(),
-          QFileInfo(fileName).fileName().toStdString(),
-          [proxy = std::move(proxy),
-           dimensions,
-           payload,
-           mimetype = mime.name().toStdString(),
-           size     = payload.size(),
-           room_id  = roomid_.toStdString(),
-           content  = std::move(bin)](const mtx::responses::ContentURI &res,
-                                     mtx::http::RequestErr err) {
-                  if (err) {
-                          emit proxy->stopLoading();
-                          emit proxy->error(
-                            tr("Failed to upload image: %s")
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                          return;
-                  }
-
-                  using namespace mtx::events;
-                  state::Avatar avatar_event;
-                  avatar_event.image_info.w        = dimensions.width();
-                  avatar_event.image_info.h        = dimensions.height();
-                  avatar_event.image_info.mimetype = mimetype;
-                  avatar_event.image_info.size     = size;
-                  avatar_event.url                 = res.content_uri;
-
-                  http::client()->send_state_event(
-                    room_id,
-                    avatar_event,
-                    [content = std::move(content), proxy = std::move(proxy)](
-                      const mtx::responses::EventId &, mtx::http::RequestErr err) {
-                            if (err) {
-                                    emit proxy->error(
-                                      tr("Failed to upload image: %s")
+    const QString picturesFolder =
+      QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
+    const QString fileName = QFileDialog::getOpenFileName(
+      nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
+
+    if (fileName.isEmpty())
+        return;
+
+    QMimeDatabase db;
+    QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
+
+    const auto format = mime.name().split("/")[0];
+
+    QFile file{fileName, this};
+    if (format != "image") {
+        emit displayError(tr("The selected file is not an image"));
+        return;
+    }
+
+    if (!file.open(QIODevice::ReadOnly)) {
+        emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
+        return;
+    }
+
+    isLoading_ = true;
+    emit loadingChanged();
+
+    // Events emitted from the http callbacks (different threads) will
+    // be queued back into the UI thread through this proxy object.
+    auto proxy = std::make_shared<ThreadProxy>();
+    connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError);
+    connect(proxy.get(), &ThreadProxy::stopLoading, this, &RoomSettings::stopLoading);
+
+    const auto bin        = file.peek(file.size());
+    const auto payload    = std::string(bin.data(), bin.size());
+    const auto dimensions = QImageReader(&file).size();
+
+    // First we need to create a new mxc URI
+    // (i.e upload media to the Matrix content repository) for the new avatar.
+    http::client()->upload(
+      payload,
+      mime.name().toStdString(),
+      QFileInfo(fileName).fileName().toStdString(),
+      [proxy = std::move(proxy),
+       dimensions,
+       payload,
+       mimetype = mime.name().toStdString(),
+       size     = payload.size(),
+       room_id  = roomid_.toStdString(),
+       content = std::move(bin)](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
+          if (err) {
+              emit proxy->stopLoading();
+              emit proxy->error(tr("Failed to upload image: %s")
+                                  .arg(QString::fromStdString(err->matrix_error.error)));
+              return;
+          }
+
+          using namespace mtx::events;
+          state::Avatar avatar_event;
+          avatar_event.image_info.w        = dimensions.width();
+          avatar_event.image_info.h        = dimensions.height();
+          avatar_event.image_info.mimetype = mimetype;
+          avatar_event.image_info.size     = size;
+          avatar_event.url                 = res.content_uri;
+
+          http::client()->send_state_event(
+            room_id,
+            avatar_event,
+            [content = std::move(content),
+             proxy = std::move(proxy)](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+                if (err) {
+                    emit proxy->error(tr("Failed to upload image: %s")
                                         .arg(QString::fromStdString(err->matrix_error.error)));
-                                    return;
-                            }
+                    return;
+                }
 
-                            emit proxy->stopLoading();
-                    });
-          });
+                emit proxy->stopLoading();
+            });
+      });
 }
diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h
index 1c8b47d63d66896bae3f59ed0ba46eb3f343843a..6ec645e01ade88734d8cce7c8740283b2553b558 100644
--- a/src/ui/RoomSettings.h
+++ b/src/ui/RoomSettings.h
@@ -19,117 +19,121 @@ class TextField;
 /// outside of main with the UI code.
 class ThreadProxy : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 signals:
-        void error(const QString &msg);
-        void nameEventSent(const QString &);
-        void topicEventSent(const QString &);
-        void stopLoading();
+    void error(const QString &msg);
+    void nameEventSent(const QString &);
+    void topicEventSent(const QString &);
+    void stopLoading();
 };
 
 class EditModal : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        EditModal(const QString &roomId, QWidget *parent = nullptr);
+    EditModal(const QString &roomId, QWidget *parent = nullptr);
 
-        void setFields(const QString &roomName, const QString &roomTopic);
+    void setFields(const QString &roomName, const QString &roomTopic);
 
 signals:
-        void nameChanged(const QString &roomName);
-        void topicChanged(const QString &topic);
+    void nameChanged(const QString &roomName);
+    void topicChanged(const QString &topic);
 
 private slots:
-        void topicEventSent(const QString &topic);
-        void nameEventSent(const QString &name);
-        void error(const QString &msg);
+    void topicEventSent(const QString &topic);
+    void nameEventSent(const QString &name);
+    void error(const QString &msg);
 
-        void applyClicked();
+    void applyClicked();
 
 private:
-        QString roomId_;
-        QString initialName_;
-        QString initialTopic_;
+    QString roomId_;
+    QString initialName_;
+    QString initialTopic_;
 
-        QLabel *errorField_;
+    QLabel *errorField_;
 
-        TextField *nameInput_;
-        TextField *topicInput_;
+    TextField *nameInput_;
+    TextField *topicInput_;
 
-        QPushButton *applyBtn_;
-        QPushButton *cancelBtn_;
+    QPushButton *applyBtn_;
+    QPushButton *cancelBtn_;
 };
 
 class RoomSettings : public QObject
 {
-        Q_OBJECT
-        Q_PROPERTY(QString roomId READ roomId CONSTANT)
-        Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT)
-        Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
-        Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
-        Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged)
-        Q_PROPERTY(int memberCount READ memberCount CONSTANT)
-        Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged)
-        Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged)
-        Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
-        Q_PROPERTY(bool canChangeAvatar READ canChangeAvatar CONSTANT)
-        Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT)
-        Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT)
-        Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged)
+    Q_OBJECT
+    Q_PROPERTY(QString roomId READ roomId CONSTANT)
+    Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT)
+    Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+    Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
+    Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged)
+    Q_PROPERTY(int memberCount READ memberCount CONSTANT)
+    Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged)
+    Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged)
+    Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
+    Q_PROPERTY(bool canChangeAvatar READ canChangeAvatar CONSTANT)
+    Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT)
+    Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT)
+    Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged)
+    Q_PROPERTY(bool supportsKnocking READ supportsKnocking CONSTANT)
+    Q_PROPERTY(bool supportsRestricted READ supportsRestricted CONSTANT)
 
 public:
-        RoomSettings(QString roomid, QObject *parent = nullptr);
-
-        QString roomId() const;
-        QString roomName() const;
-        QString roomTopic() const;
-        QString roomVersion() const;
-        QString roomAvatarUrl();
-        int memberCount() const;
-        int notifications();
-        int accessJoinRules();
-        bool isLoading() const;
-        //! Whether the user has enough power level to send m.room.join_rules events.
-        bool canChangeJoinRules() const;
-        //! Whether the user has enough power level to send m.room.name & m.room.topic events.
-        bool canChangeNameAndTopic() const;
-        //! Whether the user has enough power level to send m.room.avatar event.
-        bool canChangeAvatar() const;
-        bool isEncryptionEnabled() const;
-
-        Q_INVOKABLE void enableEncryption();
-        Q_INVOKABLE void updateAvatar();
-        Q_INVOKABLE void openEditModal();
-        Q_INVOKABLE void changeAccessRules(int index);
-        Q_INVOKABLE void changeNotifications(int currentIndex);
+    RoomSettings(QString roomid, QObject *parent = nullptr);
+
+    QString roomId() const;
+    QString roomName() const;
+    QString roomTopic() const;
+    QString roomVersion() const;
+    QString roomAvatarUrl();
+    int memberCount() const;
+    int notifications();
+    int accessJoinRules();
+    bool isLoading() const;
+    //! Whether the user has enough power level to send m.room.join_rules events.
+    bool canChangeJoinRules() const;
+    //! Whether the user has enough power level to send m.room.name & m.room.topic events.
+    bool canChangeNameAndTopic() const;
+    //! Whether the user has enough power level to send m.room.avatar event.
+    bool canChangeAvatar() const;
+    bool isEncryptionEnabled() const;
+    bool supportsKnocking() const;
+    bool supportsRestricted() const;
+
+    Q_INVOKABLE void enableEncryption();
+    Q_INVOKABLE void updateAvatar();
+    Q_INVOKABLE void openEditModal();
+    Q_INVOKABLE void changeAccessRules(int index);
+    Q_INVOKABLE void changeNotifications(int currentIndex);
 
 signals:
-        void loadingChanged();
-        void roomNameChanged();
-        void roomTopicChanged();
-        void avatarUrlChanged();
-        void encryptionChanged();
-        void notificationsChanged();
-        void accessJoinRulesChanged();
-        void displayError(const QString &errorMessage);
+    void loadingChanged();
+    void roomNameChanged();
+    void roomTopicChanged();
+    void avatarUrlChanged();
+    void encryptionChanged();
+    void notificationsChanged();
+    void accessJoinRulesChanged();
+    void displayError(const QString &errorMessage);
 
 public slots:
-        void stopLoading();
-        void avatarChanged();
+    void stopLoading();
+    void avatarChanged();
 
 private:
-        void retrieveRoomInfo();
-        void updateAccessRules(const std::string &room_id,
-                               const mtx::events::state::JoinRules &,
-                               const mtx::events::state::GuestAccess &);
+    void retrieveRoomInfo();
+    void updateAccessRules(const std::string &room_id,
+                           const mtx::events::state::JoinRules &,
+                           const mtx::events::state::GuestAccess &);
 
 private:
-        QString roomid_;
-        bool usesEncryption_ = false;
-        bool isLoading_      = false;
-        RoomInfo info_;
-        int notifications_ = 0;
-        int accessRules_   = 0;
+    QString roomid_;
+    bool usesEncryption_ = false;
+    bool isLoading_      = false;
+    RoomInfo info_;
+    int notifications_ = 0;
+    int accessRules_   = 0;
 };
diff --git a/src/ui/SnackBar.cpp b/src/ui/SnackBar.cpp
index 18990c4761e53986b5b16eeb391d18fc84e02e76..90187154f3bcfa1de177a699c63c7f8a35bf57f1 100644
--- a/src/ui/SnackBar.cpp
+++ b/src/ui/SnackBar.cpp
@@ -15,121 +15,121 @@ SnackBar::SnackBar(QWidget *parent)
   : OverlayWidget(parent)
   , offset_anim(this, "offset", this)
 {
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * 1.2);
-        font.setWeight(50);
-        setFont(font);
+    QFont font;
+    font.setPointSizeF(font.pointSizeF() * 1.2);
+    font.setWeight(50);
+    setFont(font);
 
-        boxHeight_ = QFontMetrics(font).height() * 2;
-        offset_    = STARTING_OFFSET;
-        position_  = SnackBarPosition::Top;
+    boxHeight_ = QFontMetrics(font).height() * 2;
+    offset_    = STARTING_OFFSET;
+    position_  = SnackBarPosition::Top;
 
-        hideTimer_.setSingleShot(true);
+    hideTimer_.setSingleShot(true);
 
-        offset_anim.setStartValue(1.0);
-        offset_anim.setEndValue(0.0);
-        offset_anim.setDuration(100);
-        offset_anim.setEasingCurve(QEasingCurve::OutCubic);
+    offset_anim.setStartValue(1.0);
+    offset_anim.setEndValue(0.0);
+    offset_anim.setDuration(100);
+    offset_anim.setEasingCurve(QEasingCurve::OutCubic);
 
-        connect(this, &SnackBar::offsetChanged, this, [this]() mutable { repaint(); });
-        connect(
-          &offset_anim, &QPropertyAnimation::finished, this, [this]() { hideTimer_.start(10000); });
+    connect(this, &SnackBar::offsetChanged, this, [this]() mutable { repaint(); });
+    connect(
+      &offset_anim, &QPropertyAnimation::finished, this, [this]() { hideTimer_.start(10000); });
 
-        connect(&hideTimer_, SIGNAL(timeout()), this, SLOT(hideMessage()));
+    connect(&hideTimer_, SIGNAL(timeout()), this, SLOT(hideMessage()));
 
-        hide();
+    hide();
 }
 
 void
 SnackBar::start()
 {
-        if (messages_.empty())
-                return;
+    if (messages_.empty())
+        return;
 
-        show();
-        raise();
+    show();
+    raise();
 
-        offset_anim.start();
+    offset_anim.start();
 }
 
 void
 SnackBar::hideMessage()
 {
-        stopTimers();
-        hide();
+    stopTimers();
+    hide();
 
-        if (!messages_.empty())
-                // Moving on to the next message.
-                messages_.pop_front();
+    if (!messages_.empty())
+        // Moving on to the next message.
+        messages_.pop_front();
 
-        // Resetting the starting position of the widget.
-        offset_ = STARTING_OFFSET;
+    // Resetting the starting position of the widget.
+    offset_ = STARTING_OFFSET;
 
-        if (!messages_.empty())
-                start();
+    if (!messages_.empty())
+        start();
 }
 
 void
 SnackBar::stopTimers()
 {
-        hideTimer_.stop();
+    hideTimer_.stop();
 }
 
 void
 SnackBar::showMessage(const QString &msg)
 {
-        messages_.push_back(msg);
+    messages_.push_back(msg);
 
-        // There is already an active message.
-        if (isVisible())
-                return;
+    // There is already an active message.
+    if (isVisible())
+        return;
 
-        start();
+    start();
 }
 
 void
 SnackBar::mousePressEvent(QMouseEvent *)
 {
-        hideMessage();
+    hideMessage();
 }
 
 void
 SnackBar::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event)
+    Q_UNUSED(event)
 
-        if (messages_.empty())
-                return;
+    if (messages_.empty())
+        return;
 
-        auto message_ = messages_.front();
+    auto message_ = messages_.front();
 
-        QPainter p(this);
-        p.setRenderHint(QPainter::Antialiasing);
+    QPainter p(this);
+    p.setRenderHint(QPainter::Antialiasing);
 
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
-        brush.setColor(bgColor_);
-        p.setBrush(brush);
+    QBrush brush;
+    brush.setStyle(Qt::SolidPattern);
+    brush.setColor(bgColor_);
+    p.setBrush(brush);
 
-        QRect r(0, 0, std::max(MIN_WIDTH, width() * MIN_WIDTH_PERCENTAGE), boxHeight_);
+    QRect r(0, 0, std::max(MIN_WIDTH, width() * MIN_WIDTH_PERCENTAGE), boxHeight_);
 
-        p.setPen(Qt::white);
-        QRect br = p.boundingRect(r, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
+    p.setPen(Qt::white);
+    QRect br = p.boundingRect(r, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
 
-        p.setPen(Qt::NoPen);
-        r = br.united(r).adjusted(-BOX_PADDING, -BOX_PADDING, BOX_PADDING, BOX_PADDING);
+    p.setPen(Qt::NoPen);
+    r = br.united(r).adjusted(-BOX_PADDING, -BOX_PADDING, BOX_PADDING, BOX_PADDING);
 
-        const qreal s = 1 - offset_;
+    const qreal s = 1 - offset_;
 
-        if (position_ == SnackBarPosition::Bottom)
-                p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
-                            height() - BOX_PADDING - s * (r.height()));
-        else
-                p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
-                            s * (r.height()) - 2 * BOX_PADDING);
+    if (position_ == SnackBarPosition::Bottom)
+        p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
+                    height() - BOX_PADDING - s * (r.height()));
+    else
+        p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
+                    s * (r.height()) - 2 * BOX_PADDING);
 
-        br.moveCenter(r.center());
-        p.drawRoundedRect(r.adjusted(0, 0, 0, 4), 4, 4);
-        p.setPen(textColor_);
-        p.drawText(br, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
+    br.moveCenter(r.center());
+    p.drawRoundedRect(r.adjusted(0, 0, 0, 4), 4, 4);
+    p.setPen(textColor_);
+    p.drawText(br, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
 }
diff --git a/src/ui/SnackBar.h b/src/ui/SnackBar.h
index 8d1279339fec3384fb6231459b470d53a81b1760..0459950f1ec68f64e8a220b02840167a8a0664d0 100644
--- a/src/ui/SnackBar.h
+++ b/src/ui/SnackBar.h
@@ -14,79 +14,79 @@
 
 enum class SnackBarPosition
 {
-        Bottom,
-        Top,
+    Bottom,
+    Top,
 };
 
 class SnackBar : public OverlayWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor bgColor READ backgroundColor WRITE setBackgroundColor)
-        Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor)
-        Q_PROPERTY(double offset READ offset WRITE setOffset NOTIFY offsetChanged)
+    Q_PROPERTY(QColor bgColor READ backgroundColor WRITE setBackgroundColor)
+    Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor)
+    Q_PROPERTY(double offset READ offset WRITE setOffset NOTIFY offsetChanged)
 
 public:
-        explicit SnackBar(QWidget *parent);
-
-        QColor backgroundColor() const { return bgColor_; }
-        void setBackgroundColor(const QColor &color)
-        {
-                bgColor_ = color;
-                update();
-        }
-
-        QColor textColor() const { return textColor_; }
-        void setTextColor(const QColor &color)
-        {
-                textColor_ = color;
-                update();
-        }
-        void setPosition(SnackBarPosition pos)
-        {
-                position_ = pos;
-                update();
-        }
-
-        double offset() { return offset_; }
-        void setOffset(double offset)
-        {
-                if (offset != offset_) {
-                        offset_ = offset;
-                        emit offsetChanged();
-                }
+    explicit SnackBar(QWidget *parent);
+
+    QColor backgroundColor() const { return bgColor_; }
+    void setBackgroundColor(const QColor &color)
+    {
+        bgColor_ = color;
+        update();
+    }
+
+    QColor textColor() const { return textColor_; }
+    void setTextColor(const QColor &color)
+    {
+        textColor_ = color;
+        update();
+    }
+    void setPosition(SnackBarPosition pos)
+    {
+        position_ = pos;
+        update();
+    }
+
+    double offset() { return offset_; }
+    void setOffset(double offset)
+    {
+        if (offset != offset_) {
+            offset_ = offset;
+            emit offsetChanged();
         }
+    }
 
 public slots:
-        void showMessage(const QString &msg);
+    void showMessage(const QString &msg);
 
 signals:
-        void offsetChanged();
+    void offsetChanged();
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
+    void mousePressEvent(QMouseEvent *event) override;
 
 private slots:
-        void hideMessage();
+    void hideMessage();
 
 private:
-        void stopTimers();
-        void start();
+    void stopTimers();
+    void start();
 
-        QColor bgColor_;
-        QColor textColor_;
+    QColor bgColor_;
+    QColor textColor_;
 
-        qreal bgOpacity_;
-        qreal offset_;
+    qreal bgOpacity_;
+    qreal offset_;
 
-        std::deque<QString> messages_;
+    std::deque<QString> messages_;
 
-        QTimer hideTimer_;
+    QTimer hideTimer_;
 
-        double boxHeight_;
+    double boxHeight_;
 
-        QPropertyAnimation offset_anim;
+    QPropertyAnimation offset_anim;
 
-        SnackBarPosition position_;
+    SnackBarPosition position_;
 };
diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp
index 7d015e893f797ad55efbcc5dbccb05cb299bed3d..8f1a6aa58689f3b42a14ec365d336601b647bb46 100644
--- a/src/ui/TextField.cpp
+++ b/src/ui/TextField.cpp
@@ -15,363 +15,363 @@
 TextField::TextField(QWidget *parent)
   : QLineEdit(parent)
 {
-        // Get rid of the focus border on macOS.
-        setAttribute(Qt::WA_MacShowFocusRect, 0);
+    // Get rid of the focus border on macOS.
+    setAttribute(Qt::WA_MacShowFocusRect, 0);
 
-        QPalette pal;
+    QPalette pal;
 
-        state_machine_    = new TextFieldStateMachine(this);
-        label_            = nullptr;
-        label_font_size_  = 15;
-        show_label_       = false;
-        background_color_ = pal.color(QPalette::Window);
-        is_valid_         = true;
+    state_machine_    = new TextFieldStateMachine(this);
+    label_            = nullptr;
+    label_font_size_  = 15;
+    show_label_       = false;
+    background_color_ = pal.color(QPalette::Window);
+    is_valid_         = true;
 
-        setFrame(false);
-        setAttribute(Qt::WA_Hover);
-        setMouseTracking(true);
-        setTextMargins(0, 4, 0, 6);
+    setFrame(false);
+    setAttribute(Qt::WA_Hover);
+    setMouseTracking(true);
+    setTextMargins(0, 4, 0, 6);
 
-        state_machine_->start();
-        QCoreApplication::processEvents();
+    state_machine_->start();
+    QCoreApplication::processEvents();
 }
 
 void
 TextField::setBackgroundColor(const QColor &color)
 {
-        background_color_ = color;
+    background_color_ = color;
 }
 
 QColor
 TextField::backgroundColor() const
 {
-        return background_color_;
+    return background_color_;
 }
 
 void
 TextField::setShowLabel(bool value)
 {
-        if (show_label_ == value) {
-                return;
-        }
-
-        show_label_ = value;
-
-        if (!label_ && value) {
-                label_ = new TextFieldLabel(this);
-                state_machine_->setLabel(label_);
-        }
-
-        if (value) {
-                setContentsMargins(0, 23, 0, 0);
-        } else {
-                setContentsMargins(0, 0, 0, 0);
-        }
+    if (show_label_ == value) {
+        return;
+    }
+
+    show_label_ = value;
+
+    if (!label_ && value) {
+        label_ = new TextFieldLabel(this);
+        state_machine_->setLabel(label_);
+    }
+
+    if (value) {
+        setContentsMargins(0, 23, 0, 0);
+    } else {
+        setContentsMargins(0, 0, 0, 0);
+    }
 }
 
 bool
 TextField::hasLabel() const
 {
-        return show_label_;
+    return show_label_;
 }
 
 void
 TextField::setValid(bool valid)
 {
-        is_valid_ = valid;
+    is_valid_ = valid;
 }
 
 bool
 TextField::isValid() const
 {
-        QString s = text();
-        int pos   = 0;
-        if (regexp_.pattern().isEmpty()) {
-                return is_valid_;
-        }
-        QRegularExpressionValidator v(QRegularExpression(regexp_), 0);
-        return v.validate(s, pos) == QValidator::Acceptable;
+    QString s = text();
+    int pos   = 0;
+    if (regexp_.pattern().isEmpty()) {
+        return is_valid_;
+    }
+    QRegularExpressionValidator v(QRegularExpression(regexp_), 0);
+    return v.validate(s, pos) == QValidator::Acceptable;
 }
 
 void
 TextField::setLabelFontSize(qreal size)
 {
-        label_font_size_ = size;
+    label_font_size_ = size;
 
-        if (label_) {
-                QFont font(label_->font());
-                font.setPointSizeF(size);
-                label_->setFont(font);
-                label_->update();
-        }
+    if (label_) {
+        QFont font(label_->font());
+        font.setPointSizeF(size);
+        label_->setFont(font);
+        label_->update();
+    }
 }
 
 qreal
 TextField::labelFontSize() const
 {
-        return label_font_size_;
+    return label_font_size_;
 }
 
 void
 TextField::setLabel(const QString &label)
 {
-        label_text_ = label;
-        setShowLabel(true);
-        label_->update();
+    label_text_ = label;
+    setShowLabel(true);
+    label_->update();
 }
 
 QString
 TextField::label() const
 {
-        return label_text_;
+    return label_text_;
 }
 
 void
 TextField::setLabelColor(const QColor &color)
 {
-        label_color_ = color;
-        update();
+    label_color_ = color;
+    update();
 }
 
 QColor
 TextField::labelColor() const
 {
-        if (!label_color_.isValid()) {
-                return QPalette().color(QPalette::Text);
-        }
+    if (!label_color_.isValid()) {
+        return QPalette().color(QPalette::Text);
+    }
 
-        return label_color_;
+    return label_color_;
 }
 
 void
 TextField::setInkColor(const QColor &color)
 {
-        ink_color_ = color;
-        update();
+    ink_color_ = color;
+    update();
 }
 
 QColor
 TextField::inkColor() const
 {
-        if (!ink_color_.isValid()) {
-                return QPalette().color(QPalette::Text);
-        }
+    if (!ink_color_.isValid()) {
+        return QPalette().color(QPalette::Text);
+    }
 
-        return ink_color_;
+    return ink_color_;
 }
 
 void
 TextField::setUnderlineColor(const QColor &color)
 {
-        underline_color_ = color;
-        update();
+    underline_color_ = color;
+    update();
 }
 
 void
 TextField::setRegexp(const QRegularExpression &regexp)
 {
-        regexp_ = regexp;
+    regexp_ = regexp;
 }
 
 QColor
 TextField::underlineColor() const
 {
-        if (!underline_color_.isValid()) {
-                if ((hasAcceptableInput() && isValid()) || !isModified())
-                        return QPalette().color(QPalette::Highlight);
-                else
-                        return Qt::red;
-        }
-
-        return underline_color_;
+    if (!underline_color_.isValid()) {
+        if ((hasAcceptableInput() && isValid()) || !isModified())
+            return QPalette().color(QPalette::Highlight);
+        else
+            return Qt::red;
+    }
+
+    return underline_color_;
 }
 
 bool
 TextField::event(QEvent *event)
 {
-        switch (event->type()) {
-        case QEvent::Resize:
-        case QEvent::Move: {
-                if (label_)
-                        label_->setGeometry(rect());
-                break;
-        }
-        default:
-                break;
-        }
-
-        return QLineEdit::event(event);
+    switch (event->type()) {
+    case QEvent::Resize:
+    case QEvent::Move: {
+        if (label_)
+            label_->setGeometry(rect());
+        break;
+    }
+    default:
+        break;
+    }
+
+    return QLineEdit::event(event);
 }
 
 void
 TextField::paintEvent(QPaintEvent *event)
 {
-        QLineEdit::paintEvent(event);
+    QLineEdit::paintEvent(event);
 
-        QPainter painter(this);
+    QPainter painter(this);
 
-        if (text().isEmpty()) {
-                painter.setOpacity(1 - state_machine_->progress());
-                painter.fillRect(rect(), backgroundColor());
-        }
+    if (text().isEmpty()) {
+        painter.setOpacity(1 - state_machine_->progress());
+        painter.fillRect(rect(), backgroundColor());
+    }
 
-        const int y  = height() - 1;
-        const int wd = width() - 5;
+    const int y  = height() - 1;
+    const int wd = width() - 5;
 
-        QPen pen;
-        pen.setWidth(1);
-        pen.setColor(underlineColor());
-        painter.setPen(pen);
-        painter.setOpacity(1);
-        painter.drawLine(2, y, wd, y);
+    QPen pen;
+    pen.setWidth(1);
+    pen.setColor(underlineColor());
+    painter.setPen(pen);
+    painter.setOpacity(1);
+    painter.drawLine(2, y, wd, y);
 
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
-        brush.setColor(inkColor());
+    QBrush brush;
+    brush.setStyle(Qt::SolidPattern);
+    brush.setColor(inkColor());
 
-        const qreal progress = state_machine_->progress();
+    const qreal progress = state_machine_->progress();
 
-        if (progress > 0) {
-                painter.setPen(Qt::NoPen);
-                painter.setBrush(brush);
-                const int w = (1 - progress) * static_cast<qreal>(wd / 2);
-                painter.drawRect(w + 2.5, height() - 2, wd - 2 * w, 2);
-        }
+    if (progress > 0) {
+        painter.setPen(Qt::NoPen);
+        painter.setBrush(brush);
+        const int w = (1 - progress) * static_cast<qreal>(wd / 2);
+        painter.drawRect(w + 2.5, height() - 2, wd - 2 * w, 2);
+    }
 }
 
 TextFieldStateMachine::TextFieldStateMachine(TextField *parent)
   : QStateMachine(parent)
   , text_field_(parent)
 {
-        normal_state_  = new QState;
-        focused_state_ = new QState;
+    normal_state_  = new QState;
+    focused_state_ = new QState;
 
-        label_       = nullptr;
-        offset_anim_ = nullptr;
-        color_anim_  = nullptr;
-        progress_    = 0.0;
+    label_       = nullptr;
+    offset_anim_ = nullptr;
+    color_anim_  = nullptr;
+    progress_    = 0.0;
 
-        addState(normal_state_);
-        addState(focused_state_);
+    addState(normal_state_);
+    addState(focused_state_);
 
-        setInitialState(normal_state_);
+    setInitialState(normal_state_);
 
-        QEventTransition *transition;
-        QPropertyAnimation *animation;
+    QEventTransition *transition;
+    QPropertyAnimation *animation;
 
-        transition = new QEventTransition(parent, QEvent::FocusIn);
-        transition->setTargetState(focused_state_);
-        normal_state_->addTransition(transition);
+    transition = new QEventTransition(parent, QEvent::FocusIn);
+    transition->setTargetState(focused_state_);
+    normal_state_->addTransition(transition);
 
-        animation = new QPropertyAnimation(this, "progress", this);
-        animation->setEasingCurve(QEasingCurve::InCubic);
-        animation->setDuration(310);
-        transition->addAnimation(animation);
+    animation = new QPropertyAnimation(this, "progress", this);
+    animation->setEasingCurve(QEasingCurve::InCubic);
+    animation->setDuration(310);
+    transition->addAnimation(animation);
 
-        transition = new QEventTransition(parent, QEvent::FocusOut);
-        transition->setTargetState(normal_state_);
-        focused_state_->addTransition(transition);
+    transition = new QEventTransition(parent, QEvent::FocusOut);
+    transition->setTargetState(normal_state_);
+    focused_state_->addTransition(transition);
 
-        animation = new QPropertyAnimation(this, "progress", this);
-        animation->setEasingCurve(QEasingCurve::OutCubic);
-        animation->setDuration(310);
-        transition->addAnimation(animation);
+    animation = new QPropertyAnimation(this, "progress", this);
+    animation->setEasingCurve(QEasingCurve::OutCubic);
+    animation->setDuration(310);
+    transition->addAnimation(animation);
 
-        normal_state_->assignProperty(this, "progress", 0);
-        focused_state_->assignProperty(this, "progress", 1);
+    normal_state_->assignProperty(this, "progress", 0);
+    focused_state_->assignProperty(this, "progress", 1);
 
-        setupProperties();
+    setupProperties();
 
-        connect(text_field_, SIGNAL(textChanged(QString)), this, SLOT(setupProperties()));
+    connect(text_field_, SIGNAL(textChanged(QString)), this, SLOT(setupProperties()));
 }
 
 void
 TextFieldStateMachine::setLabel(TextFieldLabel *label)
 {
-        if (label_) {
-                delete label_;
-        }
-
-        if (offset_anim_) {
-                removeDefaultAnimation(offset_anim_);
-                delete offset_anim_;
-        }
-
-        if (color_anim_) {
-                removeDefaultAnimation(color_anim_);
-                delete color_anim_;
-        }
-
-        label_ = label;
-
-        if (label_) {
-                offset_anim_ = new QPropertyAnimation(label_, "offset", this);
-                offset_anim_->setDuration(210);
-                offset_anim_->setEasingCurve(QEasingCurve::OutCubic);
-                addDefaultAnimation(offset_anim_);
-
-                color_anim_ = new QPropertyAnimation(label_, "color", this);
-                color_anim_->setDuration(210);
-                addDefaultAnimation(color_anim_);
-        }
-
-        setupProperties();
+    if (label_) {
+        delete label_;
+    }
+
+    if (offset_anim_) {
+        removeDefaultAnimation(offset_anim_);
+        delete offset_anim_;
+    }
+
+    if (color_anim_) {
+        removeDefaultAnimation(color_anim_);
+        delete color_anim_;
+    }
+
+    label_ = label;
+
+    if (label_) {
+        offset_anim_ = new QPropertyAnimation(label_, "offset", this);
+        offset_anim_->setDuration(210);
+        offset_anim_->setEasingCurve(QEasingCurve::OutCubic);
+        addDefaultAnimation(offset_anim_);
+
+        color_anim_ = new QPropertyAnimation(label_, "color", this);
+        color_anim_->setDuration(210);
+        addDefaultAnimation(color_anim_);
+    }
+
+    setupProperties();
 }
 
 void
 TextFieldStateMachine::setupProperties()
 {
-        if (label_) {
-                const int m = text_field_->textMargins().top();
-
-                if (text_field_->text().isEmpty()) {
-                        normal_state_->assignProperty(label_, "offset", QPointF(0, 26));
-                } else {
-                        normal_state_->assignProperty(label_, "offset", QPointF(0, 0 - m));
-                }
-
-                focused_state_->assignProperty(label_, "offset", QPointF(0, 0 - m));
-                focused_state_->assignProperty(label_, "color", text_field_->inkColor());
-                normal_state_->assignProperty(label_, "color", text_field_->labelColor());
-
-                if (0 != label_->offset().y() && !text_field_->text().isEmpty()) {
-                        label_->setOffset(QPointF(0, 0 - m));
-                } else if (!text_field_->hasFocus() && label_->offset().y() <= 0 &&
-                           text_field_->text().isEmpty()) {
-                        label_->setOffset(QPointF(0, 26));
-                }
+    if (label_) {
+        const int m = text_field_->textMargins().top();
+
+        if (text_field_->text().isEmpty()) {
+            normal_state_->assignProperty(label_, "offset", QPointF(0, 26));
+        } else {
+            normal_state_->assignProperty(label_, "offset", QPointF(0, 0 - m));
+        }
+
+        focused_state_->assignProperty(label_, "offset", QPointF(0, 0 - m));
+        focused_state_->assignProperty(label_, "color", text_field_->inkColor());
+        normal_state_->assignProperty(label_, "color", text_field_->labelColor());
+
+        if (0 != label_->offset().y() && !text_field_->text().isEmpty()) {
+            label_->setOffset(QPointF(0, 0 - m));
+        } else if (!text_field_->hasFocus() && label_->offset().y() <= 0 &&
+                   text_field_->text().isEmpty()) {
+            label_->setOffset(QPointF(0, 26));
         }
+    }
 
-        text_field_->update();
+    text_field_->update();
 }
 
 TextFieldLabel::TextFieldLabel(TextField *parent)
   : QWidget(parent)
   , text_field_(parent)
 {
-        x_     = 0;
-        y_     = 26;
-        scale_ = 1;
-        color_ = parent->labelColor();
-
-        QFont font;
-        font.setWeight(60);
-        font.setLetterSpacing(QFont::PercentageSpacing, 102);
-        setFont(font);
+    x_     = 0;
+    y_     = 26;
+    scale_ = 1;
+    color_ = parent->labelColor();
+
+    QFont font;
+    font.setWeight(60);
+    font.setLetterSpacing(QFont::PercentageSpacing, 102);
+    setFont(font);
 }
 
 void
 TextFieldLabel::paintEvent(QPaintEvent *)
 {
-        if (!text_field_->hasLabel())
-                return;
+    if (!text_field_->hasLabel())
+        return;
 
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-        painter.scale(scale_, scale_);
-        painter.setPen(color_);
-        painter.setOpacity(1);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
+    painter.scale(scale_, scale_);
+    painter.setPen(color_);
+    painter.setOpacity(1);
 
-        QPointF pos(2 + x_, height() - 36 + y_);
-        painter.drawText(pos.x(), pos.y(), text_field_->label());
+    QPointF pos(2 + x_, height() - 36 + y_);
+    painter.drawText(pos.x(), pos.y(), text_field_->label());
 }
diff --git a/src/ui/TextField.h b/src/ui/TextField.h
index ac4c396ebbc1b34cb7481cd6211e2a5d2ebecbff..47257019401b4b062c3ca6b876b198cc205cc5ea 100644
--- a/src/ui/TextField.h
+++ b/src/ui/TextField.h
@@ -18,163 +18,163 @@ class TextFieldStateMachine;
 
 class TextField : public QLineEdit
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor)
-        Q_PROPERTY(QColor labelColor WRITE setLabelColor READ labelColor)
-        Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
+    Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor)
+    Q_PROPERTY(QColor labelColor WRITE setLabelColor READ labelColor)
+    Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor)
+    Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
 
 public:
-        explicit TextField(QWidget *parent = nullptr);
-
-        void setInkColor(const QColor &color);
-        void setBackgroundColor(const QColor &color);
-        void setLabel(const QString &label);
-        void setLabelColor(const QColor &color);
-        void setLabelFontSize(qreal size);
-        void setShowLabel(bool value);
-        void setUnderlineColor(const QColor &color);
-        void setRegexp(const QRegularExpression &regexp);
-        void setValid(bool valid);
-
-        QColor inkColor() const;
-        QColor labelColor() const;
-        QColor underlineColor() const;
-        QColor backgroundColor() const;
-        QString label() const;
-        bool hasLabel() const;
-        bool isValid() const;
-        qreal labelFontSize() const;
+    explicit TextField(QWidget *parent = nullptr);
+
+    void setInkColor(const QColor &color);
+    void setBackgroundColor(const QColor &color);
+    void setLabel(const QString &label);
+    void setLabelColor(const QColor &color);
+    void setLabelFontSize(qreal size);
+    void setShowLabel(bool value);
+    void setUnderlineColor(const QColor &color);
+    void setRegexp(const QRegularExpression &regexp);
+    void setValid(bool valid);
+
+    QColor inkColor() const;
+    QColor labelColor() const;
+    QColor underlineColor() const;
+    QColor backgroundColor() const;
+    QString label() const;
+    bool hasLabel() const;
+    bool isValid() const;
+    qreal labelFontSize() const;
 
 protected:
-        bool event(QEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
+    bool event(QEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 private:
-        void init();
-
-        QColor ink_color_;
-        QColor background_color_;
-        QColor label_color_;
-        QColor underline_color_;
-        QString label_text_;
-        TextFieldLabel *label_;
-        TextFieldStateMachine *state_machine_;
-        bool show_label_;
-        QRegularExpression regexp_;
-        bool is_valid_;
-        qreal label_font_size_;
+    void init();
+
+    QColor ink_color_;
+    QColor background_color_;
+    QColor label_color_;
+    QColor underline_color_;
+    QString label_text_;
+    TextFieldLabel *label_;
+    TextFieldStateMachine *state_machine_;
+    bool show_label_;
+    QRegularExpression regexp_;
+    bool is_valid_;
+    qreal label_font_size_;
 };
 
 class TextFieldLabel : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(qreal scale WRITE setScale READ scale)
-        Q_PROPERTY(QPointF offset WRITE setOffset READ offset)
-        Q_PROPERTY(QColor color WRITE setColor READ color)
+    Q_PROPERTY(qreal scale WRITE setScale READ scale)
+    Q_PROPERTY(QPointF offset WRITE setOffset READ offset)
+    Q_PROPERTY(QColor color WRITE setColor READ color)
 
 public:
-        TextFieldLabel(TextField *parent);
+    TextFieldLabel(TextField *parent);
 
-        inline void setColor(const QColor &color);
-        inline void setOffset(const QPointF &pos);
-        inline void setScale(qreal scale);
+    inline void setColor(const QColor &color);
+    inline void setOffset(const QPointF &pos);
+    inline void setScale(qreal scale);
 
-        inline QColor color() const;
-        inline QPointF offset() const;
-        inline qreal scale() const;
+    inline QColor color() const;
+    inline QPointF offset() const;
+    inline qreal scale() const;
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 private:
-        TextField *const text_field_;
+    TextField *const text_field_;
 
-        QColor color_;
-        qreal scale_;
-        qreal x_;
-        qreal y_;
+    QColor color_;
+    qreal scale_;
+    qreal x_;
+    qreal y_;
 };
 
 inline void
 TextFieldLabel::setColor(const QColor &color)
 {
-        color_ = color;
-        update();
+    color_ = color;
+    update();
 }
 
 inline void
 TextFieldLabel::setOffset(const QPointF &pos)
 {
-        x_ = pos.x();
-        y_ = pos.y();
-        update();
+    x_ = pos.x();
+    y_ = pos.y();
+    update();
 }
 
 inline void
 TextFieldLabel::setScale(qreal scale)
 {
-        scale_ = scale;
-        update();
+    scale_ = scale;
+    update();
 }
 
 inline QPointF
 TextFieldLabel::offset() const
 {
-        return QPointF(x_, y_);
+    return QPointF(x_, y_);
 }
 inline qreal
 TextFieldLabel::scale() const
 {
-        return scale_;
+    return scale_;
 }
 inline QColor
 TextFieldLabel::color() const
 {
-        return color_;
+    return color_;
 }
 
 class TextFieldStateMachine : public QStateMachine
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(qreal progress WRITE setProgress READ progress)
+    Q_PROPERTY(qreal progress WRITE setProgress READ progress)
 
 public:
-        TextFieldStateMachine(TextField *parent);
+    TextFieldStateMachine(TextField *parent);
 
-        inline void setProgress(qreal progress);
-        void setLabel(TextFieldLabel *label);
+    inline void setProgress(qreal progress);
+    void setLabel(TextFieldLabel *label);
 
-        inline qreal progress() const;
+    inline qreal progress() const;
 
 public slots:
-        void setupProperties();
+    void setupProperties();
 
 private:
-        QPropertyAnimation *color_anim_;
-        QPropertyAnimation *offset_anim_;
+    QPropertyAnimation *color_anim_;
+    QPropertyAnimation *offset_anim_;
 
-        QState *focused_state_;
-        QState *normal_state_;
+    QState *focused_state_;
+    QState *normal_state_;
 
-        TextField *text_field_;
-        TextFieldLabel *label_;
+    TextField *text_field_;
+    TextFieldLabel *label_;
 
-        qreal progress_;
+    qreal progress_;
 };
 
 inline void
 TextFieldStateMachine::setProgress(qreal progress)
 {
-        progress_ = progress;
-        text_field_->update();
+    progress_ = progress;
+    text_field_->update();
 }
 
 inline qreal
 TextFieldStateMachine::progress() const
 {
-        return progress_;
+    return progress_;
 }
diff --git a/src/ui/TextLabel.cpp b/src/ui/TextLabel.cpp
index 3568e15c5b00934368350fe7a6c8c61c9ebddc7f..340d3b8f1eb2d2d2ad2be69c5c87c6ead85b7cc7 100644
--- a/src/ui/TextLabel.cpp
+++ b/src/ui/TextLabel.cpp
@@ -14,12 +14,12 @@
 bool
 ContextMenuFilter::eventFilter(QObject *obj, QEvent *event)
 {
-        if (event->type() == QEvent::MouseButtonPress) {
-                emit contextMenuIsOpening();
-                return true;
-        }
+    if (event->type() == QEvent::MouseButtonPress) {
+        emit contextMenuIsOpening();
+        return true;
+    }
 
-        return QObject::eventFilter(obj, event);
+    return QObject::eventFilter(obj, event);
 }
 
 TextLabel::TextLabel(QWidget *parent)
@@ -29,94 +29,94 @@ TextLabel::TextLabel(QWidget *parent)
 TextLabel::TextLabel(const QString &text, QWidget *parent)
   : QTextBrowser(parent)
 {
-        document()->setDefaultStyleSheet(QString("a {color: %1; }").arg(utils::linkColor()));
-
-        setText(text);
-        setOpenExternalLinks(true);
-
-        // Make it look and feel like an ordinary label.
-        setReadOnly(true);
-        setFrameStyle(QFrame::NoFrame);
-        QPalette pal = palette();
-        pal.setColor(QPalette::Base, Qt::transparent);
-        setPalette(pal);
-
-        // Wrap anywhere but prefer words, adjust minimum height on the fly.
-        setLineWrapMode(QTextEdit::WidgetWidth);
-        setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
-        connect(document()->documentLayout(),
-                &QAbstractTextDocumentLayout::documentSizeChanged,
-                this,
-                &TextLabel::adjustHeight);
-        document()->setDocumentMargin(0);
-
-        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
-        setFixedHeight(0);
-
-        connect(this, &TextLabel::linkActivated, this, &TextLabel::handleLinkActivation);
-
-        auto filter = new ContextMenuFilter(this);
-        installEventFilter(filter);
-        connect(filter, &ContextMenuFilter::contextMenuIsOpening, this, [this]() {
-                contextMenuRequested_ = true;
-        });
+    document()->setDefaultStyleSheet(QString("a {color: %1; }").arg(utils::linkColor()));
+
+    setText(text);
+    setOpenExternalLinks(true);
+
+    // Make it look and feel like an ordinary label.
+    setReadOnly(true);
+    setFrameStyle(QFrame::NoFrame);
+    QPalette pal = palette();
+    pal.setColor(QPalette::Base, Qt::transparent);
+    setPalette(pal);
+
+    // Wrap anywhere but prefer words, adjust minimum height on the fly.
+    setLineWrapMode(QTextEdit::WidgetWidth);
+    setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+    connect(document()->documentLayout(),
+            &QAbstractTextDocumentLayout::documentSizeChanged,
+            this,
+            &TextLabel::adjustHeight);
+    document()->setDocumentMargin(0);
+
+    setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+    setFixedHeight(0);
+
+    connect(this, &TextLabel::linkActivated, this, &TextLabel::handleLinkActivation);
+
+    auto filter = new ContextMenuFilter(this);
+    installEventFilter(filter);
+    connect(filter, &ContextMenuFilter::contextMenuIsOpening, this, [this]() {
+        contextMenuRequested_ = true;
+    });
 }
 
 void
 TextLabel::focusOutEvent(QFocusEvent *e)
 {
-        QTextBrowser::focusOutEvent(e);
+    QTextBrowser::focusOutEvent(e);
 
-        // We keep the selection available for the context menu.
-        if (contextMenuRequested_) {
-                contextMenuRequested_ = false;
-                return;
-        }
+    // We keep the selection available for the context menu.
+    if (contextMenuRequested_) {
+        contextMenuRequested_ = false;
+        return;
+    }
 
-        QTextCursor cursor = textCursor();
-        cursor.clearSelection();
-        setTextCursor(cursor);
+    QTextCursor cursor = textCursor();
+    cursor.clearSelection();
+    setTextCursor(cursor);
 }
 
 void
 TextLabel::mousePressEvent(QMouseEvent *e)
 {
-        link_ = (e->button() & Qt::LeftButton) ? anchorAt(e->pos()) : QString();
-        QTextBrowser::mousePressEvent(e);
+    link_ = (e->button() & Qt::LeftButton) ? anchorAt(e->pos()) : QString();
+    QTextBrowser::mousePressEvent(e);
 }
 
 void
 TextLabel::mouseReleaseEvent(QMouseEvent *e)
 {
-        if (e->button() & Qt::LeftButton && !link_.isEmpty() && anchorAt(e->pos()) == link_) {
-                emit linkActivated(link_);
-                return;
-        }
+    if (e->button() & Qt::LeftButton && !link_.isEmpty() && anchorAt(e->pos()) == link_) {
+        emit linkActivated(link_);
+        return;
+    }
 
-        QTextBrowser::mouseReleaseEvent(e);
+    QTextBrowser::mouseReleaseEvent(e);
 }
 
 void
 TextLabel::wheelEvent(QWheelEvent *event)
 {
-        event->ignore();
+    event->ignore();
 }
 
 void
 TextLabel::handleLinkActivation(const QUrl &url)
 {
-        auto parts          = url.toString().split('/');
-        auto defaultHandler = [](const QUrl &url) { QDesktopServices::openUrl(url); };
+    auto parts          = url.toString().split('/');
+    auto defaultHandler = [](const QUrl &url) { QDesktopServices::openUrl(url); };
 
-        if (url.host() != "matrix.to" || parts.isEmpty())
-                return defaultHandler(url);
+    if (url.host() != "matrix.to" || parts.isEmpty())
+        return defaultHandler(url);
 
-        try {
-                using namespace mtx::identifiers;
-                parse<User>(parts.last().toStdString());
-        } catch (const std::exception &) {
-                return defaultHandler(url);
-        }
+    try {
+        using namespace mtx::identifiers;
+        parse<User>(parts.last().toStdString());
+    } catch (const std::exception &) {
+        return defaultHandler(url);
+    }
 
-        emit userProfileTriggered(parts.last());
+    emit userProfileTriggered(parts.last());
 }
diff --git a/src/ui/TextLabel.h b/src/ui/TextLabel.h
index bc0958231e91d09b6f1387ce1a31175ac703ecbd..313ad97cc1efe82ccd12bf455e02a2d86db7ff56 100644
--- a/src/ui/TextLabel.h
+++ b/src/ui/TextLabel.h
@@ -15,45 +15,45 @@ class QWheelEvent;
 
 class ContextMenuFilter : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        explicit ContextMenuFilter(QWidget *parent)
-          : QObject(parent)
-        {}
+    explicit ContextMenuFilter(QWidget *parent)
+      : QObject(parent)
+    {}
 
 signals:
-        void contextMenuIsOpening();
+    void contextMenuIsOpening();
 
 protected:
-        bool eventFilter(QObject *obj, QEvent *event) override;
+    bool eventFilter(QObject *obj, QEvent *event) override;
 };
 
 class TextLabel : public QTextBrowser
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        TextLabel(const QString &text, QWidget *parent = nullptr);
-        TextLabel(QWidget *parent = nullptr);
+    TextLabel(const QString &text, QWidget *parent = nullptr);
+    TextLabel(QWidget *parent = nullptr);
 
-        void wheelEvent(QWheelEvent *event) override;
-        void clearLinks() { link_.clear(); }
+    void wheelEvent(QWheelEvent *event) override;
+    void clearLinks() { link_.clear(); }
 
 protected:
-        void mousePressEvent(QMouseEvent *e) override;
-        void mouseReleaseEvent(QMouseEvent *e) override;
-        void focusOutEvent(QFocusEvent *e) override;
+    void mousePressEvent(QMouseEvent *e) override;
+    void mouseReleaseEvent(QMouseEvent *e) override;
+    void focusOutEvent(QFocusEvent *e) override;
 
 private slots:
-        void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); }
-        void handleLinkActivation(const QUrl &link);
+    void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); }
+    void handleLinkActivation(const QUrl &link);
 
 signals:
-        void userProfileTriggered(const QString &user_id);
-        void linkActivated(const QUrl &link);
+    void userProfileTriggered(const QString &user_id);
+    void linkActivated(const QUrl &link);
 
 private:
-        QString link_;
-        bool contextMenuRequested_ = false;
+    QString link_;
+    bool contextMenuRequested_ = false;
 };
diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index 732a044385ee4f9629c701dfdb10793019fa2308..d7c92fb88d0f1091a63ec72009075593543f80b6 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -9,66 +9,69 @@ Q_DECLARE_METATYPE(Theme)
 QPalette
 Theme::paletteFromTheme(std::string_view theme)
 {
-        [[maybe_unused]] static auto meta = qRegisterMetaType<Theme>("Theme");
-        static QPalette original;
-        if (theme == "light") {
-                QPalette lightActive(
-                  /*windowText*/ QColor("#333"),
-                  /*button*/ QColor("white"),
-                  /*light*/ QColor(0xef, 0xef, 0xef),
-                  /*dark*/ QColor(70, 77, 93),
-                  /*mid*/ QColor(220, 220, 220),
-                  /*text*/ QColor("#333"),
-                  /*bright_text*/ QColor("#f2f5f8"),
-                  /*base*/ QColor("#fff"),
-                  /*window*/ QColor("white"));
-                lightActive.setColor(QPalette::AlternateBase, QColor("#eee"));
-                lightActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
-                lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5"));
-                lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());
-                lightActive.setColor(QPalette::ToolTipText, lightActive.text().color());
-                lightActive.setColor(QPalette::Link, QColor("#0077b5"));
-                lightActive.setColor(QPalette::ButtonText, QColor("#555459"));
-                return lightActive;
-        } else if (theme == "dark") {
-                QPalette darkActive(
-                  /*windowText*/ QColor("#caccd1"),
-                  /*button*/ QColor(0xff, 0xff, 0xff),
-                  /*light*/ QColor("#caccd1"),
-                  /*dark*/ QColor(60, 70, 77),
-                  /*mid*/ QColor("#202228"),
-                  /*text*/ QColor("#caccd1"),
-                  /*bright_text*/ QColor("#f4f5f8"),
-                  /*base*/ QColor("#202228"),
-                  /*window*/ QColor("#2d3139"));
-                darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139"));
-                darkActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
-                darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8"));
-                darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
-                darkActive.setColor(QPalette::ToolTipText, darkActive.text().color());
-                darkActive.setColor(QPalette::Link, QColor("#38a3d8"));
-                darkActive.setColor(QPalette::ButtonText, "#828284");
-                return darkActive;
-        } else {
-                return original;
-        }
+    [[maybe_unused]] static auto meta = qRegisterMetaType<Theme>("Theme");
+    static QPalette original;
+    if (theme == "light") {
+        QPalette lightActive(
+          /*windowText*/ QColor("#333"),
+          /*button*/ QColor("white"),
+          /*light*/ QColor(0xef, 0xef, 0xef),
+          /*dark*/ QColor(70, 77, 93),
+          /*mid*/ QColor(220, 220, 220),
+          /*text*/ QColor("#333"),
+          /*bright_text*/ QColor("#f2f5f8"),
+          /*base*/ QColor("#fff"),
+          /*window*/ QColor("white"));
+        lightActive.setColor(QPalette::AlternateBase, QColor("#eee"));
+        lightActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
+        lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5"));
+        lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());
+        lightActive.setColor(QPalette::ToolTipText, lightActive.text().color());
+        lightActive.setColor(QPalette::Link, QColor("#0077b5"));
+        lightActive.setColor(QPalette::ButtonText, QColor("#555459"));
+        return lightActive;
+    } else if (theme == "dark") {
+        QPalette darkActive(
+          /*windowText*/ QColor("#caccd1"),
+          /*button*/ QColor(0xff, 0xff, 0xff),
+          /*light*/ QColor("#caccd1"),
+          /*dark*/ QColor(60, 70, 77),
+          /*mid*/ QColor("#202228"),
+          /*text*/ QColor("#caccd1"),
+          /*bright_text*/ QColor("#f4f5f8"),
+          /*base*/ QColor("#202228"),
+          /*window*/ QColor("#2d3139"));
+        darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139"));
+        darkActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
+        darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8"));
+        darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
+        darkActive.setColor(QPalette::ToolTipText, darkActive.text().color());
+        darkActive.setColor(QPalette::Link, QColor("#38a3d8"));
+        darkActive.setColor(QPalette::ButtonText, "#828284");
+        return darkActive;
+    } else {
+        return original;
+    }
 }
 
 Theme::Theme(std::string_view theme)
 {
-        auto p     = paletteFromTheme(theme);
-        separator_ = p.mid().color();
-        if (theme == "light") {
-                sidebarBackground_ = QColor("#233649");
-                alternateButton_   = QColor("#ccc");
-                red_               = QColor("#a82353");
-        } else if (theme == "dark") {
-                sidebarBackground_ = QColor("#2d3139");
-                alternateButton_   = QColor("#414A59");
-                red_               = QColor("#a82353");
-        } else {
-                sidebarBackground_ = p.window().color();
-                alternateButton_   = p.dark().color();
-                red_               = QColor("red");
-        }
+    auto p     = paletteFromTheme(theme);
+    separator_ = p.mid().color();
+    if (theme == "light") {
+        sidebarBackground_ = QColor("#233649");
+        alternateButton_   = QColor("#ccc");
+        red_               = QColor("#a82353");
+        orange_            = QColor("#fcbe05");
+    } else if (theme == "dark") {
+        sidebarBackground_ = QColor("#2d3139");
+        alternateButton_   = QColor("#414A59");
+        red_               = QColor("#a82353");
+        orange_            = QColor("#fcc53a");
+    } else {
+        sidebarBackground_ = p.window().color();
+        alternateButton_   = p.dark().color();
+        red_               = QColor("red");
+        orange_            = QColor("orange");
+    }
 }
diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index b5bcd4ddf42f26edb9d5df685bd9b6ff1e07fccf..4fef897d3394aa68d52ad2a7ac02e804a2b570c8 100644
--- a/src/ui/Theme.h
+++ b/src/ui/Theme.h
@@ -8,12 +8,6 @@
 #include <QPalette>
 
 namespace ui {
-enum class AvatarType
-{
-        Image,
-        Letter
-};
-
 // Default font size.
 const int FontSize = 16;
 
@@ -22,62 +16,64 @@ const int AvatarSize = 40;
 
 enum class ButtonPreset
 {
-        FlatPreset,
-        CheckablePreset
+    FlatPreset,
+    CheckablePreset
 };
 
 enum class RippleStyle
 {
-        CenteredRipple,
-        PositionedRipple,
-        NoRipple
+    CenteredRipple,
+    PositionedRipple,
+    NoRipple
 };
 
 enum class OverlayStyle
 {
-        NoOverlay,
-        TintedOverlay,
-        GrayOverlay
+    NoOverlay,
+    TintedOverlay,
+    GrayOverlay
 };
 
 enum class Role
 {
-        Default,
-        Primary,
-        Secondary
+    Default,
+    Primary,
+    Secondary
 };
 
 enum class ButtonIconPlacement
 {
-        LeftIcon,
-        RightIcon
+    LeftIcon,
+    RightIcon
 };
 
 enum class ProgressType
 {
-        DeterminateProgress,
-        IndeterminateProgress
+    DeterminateProgress,
+    IndeterminateProgress
 };
 
 } // namespace ui
 
 class Theme : public QPalette
 {
-        Q_GADGET
-        Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT)
-        Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT)
-        Q_PROPERTY(QColor separator READ separator CONSTANT)
-        Q_PROPERTY(QColor red READ red CONSTANT)
+    Q_GADGET
+    Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT)
+    Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT)
+    Q_PROPERTY(QColor separator READ separator CONSTANT)
+    Q_PROPERTY(QColor red READ red CONSTANT)
+    Q_PROPERTY(QColor orange READ orange CONSTANT)
 public:
-        Theme() {}
-        explicit Theme(std::string_view theme);
-        static QPalette paletteFromTheme(std::string_view theme);
+    Theme() {}
+    explicit Theme(std::string_view theme);
+    static QPalette paletteFromTheme(std::string_view theme);
 
-        QColor sidebarBackground() const { return sidebarBackground_; }
-        QColor alternateButton() const { return alternateButton_; }
-        QColor separator() const { return separator_; }
-        QColor red() const { return red_; }
+    QColor sidebarBackground() const { return sidebarBackground_; }
+    QColor alternateButton() const { return alternateButton_; }
+    QColor separator() const { return separator_; }
+    QColor red() const { return red_; }
+    QColor orange() const { return orange_; }
 
 private:
-        QColor sidebarBackground_, separator_, red_, alternateButton_;
+    QColor sidebarBackground_, separator_, red_, orange_, alternateButton_;
 };
diff --git a/src/ui/ThemeManager.cpp b/src/ui/ThemeManager.cpp
index b7b3df4036340e420f61ddf5b88343c9659c83cf..0c84777a741638f747ec296cc876f24738175e2c 100644
--- a/src/ui/ThemeManager.cpp
+++ b/src/ui/ThemeManager.cpp
@@ -11,32 +11,32 @@ ThemeManager::ThemeManager() {}
 QColor
 ThemeManager::themeColor(const QString &key) const
 {
-        if (key == "Black")
-                return QColor("#171919");
-
-        else if (key == "BrightWhite")
-                return QColor("#EBEBEB");
-        else if (key == "FadedWhite")
-                return QColor("#C9C9C9");
-        else if (key == "MediumWhite")
-                return QColor("#929292");
-
-        else if (key == "BrightGreen")
-                return QColor("#1C3133");
-        else if (key == "DarkGreen")
-                return QColor("#577275");
-        else if (key == "LightGreen")
-                return QColor("#46A451");
-
-        else if (key == "Gray")
-                return QColor("#5D6565");
-        else if (key == "Red")
-                return QColor("#E22826");
-        else if (key == "Blue")
-                return QColor("#81B3A9");
-
-        else if (key == "Transparent")
-                return QColor(0, 0, 0, 0);
-
-        return (QColor(0, 0, 0, 0));
+    if (key == "Black")
+        return QColor("#171919");
+
+    else if (key == "BrightWhite")
+        return QColor("#EBEBEB");
+    else if (key == "FadedWhite")
+        return QColor("#C9C9C9");
+    else if (key == "MediumWhite")
+        return QColor("#929292");
+
+    else if (key == "BrightGreen")
+        return QColor("#1C3133");
+    else if (key == "DarkGreen")
+        return QColor("#577275");
+    else if (key == "LightGreen")
+        return QColor("#46A451");
+
+    else if (key == "Gray")
+        return QColor("#5D6565");
+    else if (key == "Red")
+        return QColor("#E22826");
+    else if (key == "Blue")
+        return QColor("#81B3A9");
+
+    else if (key == "Transparent")
+        return QColor(0, 0, 0, 0);
+
+    return (QColor(0, 0, 0, 0));
 }
diff --git a/src/ui/ThemeManager.h b/src/ui/ThemeManager.h
index cbb355fde2609eca181e6c98595d87944a7e5dca..5e86c68f1259921b1cbcbf2409da6ad5a871e916 100644
--- a/src/ui/ThemeManager.h
+++ b/src/ui/ThemeManager.h
@@ -8,23 +8,23 @@
 
 class ThemeManager : public QCommonStyle
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        inline static ThemeManager &instance();
+    inline static ThemeManager &instance();
 
-        QColor themeColor(const QString &key) const;
+    QColor themeColor(const QString &key) const;
 
 private:
-        ThemeManager();
+    ThemeManager();
 
-        ThemeManager(ThemeManager const &);
-        void operator=(ThemeManager const &);
+    ThemeManager(ThemeManager const &);
+    void operator=(ThemeManager const &);
 };
 
 inline ThemeManager &
 ThemeManager::instance()
 {
-        static ThemeManager instance;
-        return instance;
+    static ThemeManager instance;
+    return instance;
 }
diff --git a/src/ui/ToggleButton.cpp b/src/ui/ToggleButton.cpp
index 33bf8f92029b732ce9d20034f029842009ddc1fe..04f752d732693c78e348e72d5c2a47959dccaa4e 100644
--- a/src/ui/ToggleButton.cpp
+++ b/src/ui/ToggleButton.cpp
@@ -12,84 +12,84 @@
 void
 Toggle::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event);
+    Q_UNUSED(event);
 }
 
 Toggle::Toggle(QWidget *parent)
   : QAbstractButton{parent}
 {
-        init();
+    init();
 
-        connect(this, &QAbstractButton::toggled, this, &Toggle::setState);
+    connect(this, &QAbstractButton::toggled, this, &Toggle::setState);
 }
 
 void
 Toggle::setState(bool isEnabled)
 {
-        setChecked(isEnabled);
-        thumb_->setShift(isEnabled ? Position::Left : Position::Right);
-        setupProperties();
+    setChecked(isEnabled);
+    thumb_->setShift(isEnabled ? Position::Left : Position::Right);
+    setupProperties();
 }
 
 void
 Toggle::init()
 {
-        track_ = new ToggleTrack(this);
-        thumb_ = new ToggleThumb(this);
+    track_ = new ToggleTrack(this);
+    thumb_ = new ToggleThumb(this);
 
-        setCursor(QCursor(Qt::PointingHandCursor));
-        setCheckable(true);
-        setChecked(false);
+    setCursor(QCursor(Qt::PointingHandCursor));
+    setCheckable(true);
+    setChecked(false);
 
-        setState(false);
-        setupProperties();
+    setState(false);
+    setupProperties();
 
-        QCoreApplication::processEvents();
+    QCoreApplication::processEvents();
 }
 
 void
 Toggle::setupProperties()
 {
-        if (isEnabled()) {
-                Position position = thumb_->shift();
+    if (isEnabled()) {
+        Position position = thumb_->shift();
 
-                thumb_->setThumbColor(trackColor());
+        thumb_->setThumbColor(trackColor());
 
-                if (position == Position::Left)
-                        track_->setTrackColor(activeColor());
-                else if (position == Position::Right)
-                        track_->setTrackColor(inactiveColor());
-        }
+        if (position == Position::Left)
+            track_->setTrackColor(activeColor());
+        else if (position == Position::Right)
+            track_->setTrackColor(inactiveColor());
+    }
 
-        update();
+    update();
 }
 
 void
 Toggle::setDisabledColor(const QColor &color)
 {
-        disabledColor_ = color;
-        setupProperties();
+    disabledColor_ = color;
+    setupProperties();
 }
 
 void
 Toggle::setActiveColor(const QColor &color)
 {
-        activeColor_ = color;
-        setupProperties();
+    activeColor_ = color;
+    setupProperties();
 }
 
 void
 Toggle::setInactiveColor(const QColor &color)
 {
-        inactiveColor_ = color;
-        setupProperties();
+    inactiveColor_ = color;
+    setupProperties();
 }
 
 void
 Toggle::setTrackColor(const QColor &color)
 {
-        trackColor_ = color;
-        setupProperties();
+    trackColor_ = color;
+    setupProperties();
 }
 
 ToggleThumb::ToggleThumb(Toggle *parent)
@@ -98,119 +98,119 @@ ToggleThumb::ToggleThumb(Toggle *parent)
   , position_{Position::Right}
   , offset_{0}
 {
-        parent->installEventFilter(this);
+    parent->installEventFilter(this);
 }
 
 void
 ToggleThumb::setShift(Position position)
 {
-        if (position_ != position) {
-                position_ = position;
-                updateOffset();
-        }
+    if (position_ != position) {
+        position_ = position;
+        updateOffset();
+    }
 }
 
 bool
 ToggleThumb::eventFilter(QObject *obj, QEvent *event)
 {
-        const QEvent::Type type = event->type();
+    const QEvent::Type type = event->type();
 
-        if (QEvent::Resize == type || QEvent::Move == type) {
-                setGeometry(toggle_->rect().adjusted(8, 8, -8, -8));
-                updateOffset();
-        }
+    if (QEvent::Resize == type || QEvent::Move == type) {
+        setGeometry(toggle_->rect().adjusted(8, 8, -8, -8));
+        updateOffset();
+    }
 
-        return QWidget::eventFilter(obj, event);
+    return QWidget::eventFilter(obj, event);
 }
 
 void
 ToggleThumb::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event)
+    Q_UNUSED(event)
 
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
 
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
-        brush.setColor(toggle_->isEnabled() ? thumbColor_ : Qt::white);
+    QBrush brush;
+    brush.setStyle(Qt::SolidPattern);
+    brush.setColor(toggle_->isEnabled() ? thumbColor_ : Qt::white);
 
-        painter.setBrush(brush);
-        painter.setPen(Qt::NoPen);
+    painter.setBrush(brush);
+    painter.setPen(Qt::NoPen);
 
-        int s;
-        QRectF r;
+    int s;
+    QRectF r;
 
-        s = height() - 10;
-        r = QRectF(5 + offset_, 5, s, s);
+    s = height() - 10;
+    r = QRectF(5 + offset_, 5, s, s);
 
-        painter.drawEllipse(r);
+    painter.drawEllipse(r);
 
-        if (!toggle_->isEnabled()) {
-                brush.setColor(toggle_->disabledColor());
-                painter.setBrush(brush);
-                painter.drawEllipse(r);
-        }
+    if (!toggle_->isEnabled()) {
+        brush.setColor(toggle_->disabledColor());
+        painter.setBrush(brush);
+        painter.drawEllipse(r);
+    }
 }
 
 void
 ToggleThumb::updateOffset()
 {
-        const QSize s(size());
-        offset_ = position_ == Position::Left ? static_cast<qreal>(s.width() - s.height()) : 0;
-        update();
+    const QSize s(size());
+    offset_ = position_ == Position::Left ? static_cast<qreal>(s.width() - s.height()) : 0;
+    update();
 }
 
 ToggleTrack::ToggleTrack(Toggle *parent)
   : QWidget{parent}
   , toggle_{parent}
 {
-        Q_ASSERT(parent);
+    Q_ASSERT(parent);
 
-        parent->installEventFilter(this);
+    parent->installEventFilter(this);
 }
 
 void
 ToggleTrack::setTrackColor(const QColor &color)
 {
-        trackColor_ = color;
-        update();
+    trackColor_ = color;
+    update();
 }
 
 bool
 ToggleTrack::eventFilter(QObject *obj, QEvent *event)
 {
-        const QEvent::Type type = event->type();
+    const QEvent::Type type = event->type();
 
-        if (QEvent::Resize == type || QEvent::Move == type) {
-                setGeometry(toggle_->rect());
-        }
+    if (QEvent::Resize == type || QEvent::Move == type) {
+        setGeometry(toggle_->rect());
+    }
 
-        return QWidget::eventFilter(obj, event);
+    return QWidget::eventFilter(obj, event);
 }
 
 void
 ToggleTrack::paintEvent(QPaintEvent *event)
 {
-        Q_UNUSED(event)
+    Q_UNUSED(event)
 
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
+    QPainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
 
-        QBrush brush;
-        if (toggle_->isEnabled()) {
-                brush.setColor(trackColor_);
-                painter.setOpacity(0.8);
-        } else {
-                brush.setColor(toggle_->disabledColor());
-                painter.setOpacity(0.6);
-        }
+    QBrush brush;
+    if (toggle_->isEnabled()) {
+        brush.setColor(trackColor_);
+        painter.setOpacity(0.8);
+    } else {
+        brush.setColor(toggle_->disabledColor());
+        painter.setOpacity(0.6);
+    }
 
-        brush.setStyle(Qt::SolidPattern);
-        painter.setBrush(brush);
-        painter.setPen(Qt::NoPen);
+    brush.setStyle(Qt::SolidPattern);
+    painter.setBrush(brush);
+    painter.setPen(Qt::NoPen);
 
-        const int h = height() / 2;
-        const QRect r(0, h / 2, width(), h);
-        painter.drawRoundedRect(r.adjusted(14, 4, -14, -4), h / 2 - 4, h / 2 - 4);
+    const int h = height() / 2;
+    const QRect r(0, h / 2, width(), h);
+    painter.drawRoundedRect(r.adjusted(14, 4, -14, -4), h / 2 - 4, h / 2 - 4);
 }
diff --git a/src/ui/ToggleButton.h b/src/ui/ToggleButton.h
index 2413b0864e9da5fc8dc2212db23a20ae6c7261ad..15d5e19230d46d1d9ad0eb7bfca0544728d88cb3 100644
--- a/src/ui/ToggleButton.h
+++ b/src/ui/ToggleButton.h
@@ -12,103 +12,103 @@ class ToggleThumb;
 
 enum class Position
 {
-        Left,
-        Right
+    Left,
+    Right
 };
 
 class Toggle : public QAbstractButton
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor activeColor WRITE setActiveColor READ activeColor)
-        Q_PROPERTY(QColor disabledColor WRITE setDisabledColor READ disabledColor)
-        Q_PROPERTY(QColor inactiveColor WRITE setInactiveColor READ inactiveColor)
-        Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor)
+    Q_PROPERTY(QColor activeColor WRITE setActiveColor READ activeColor)
+    Q_PROPERTY(QColor disabledColor WRITE setDisabledColor READ disabledColor)
+    Q_PROPERTY(QColor inactiveColor WRITE setInactiveColor READ inactiveColor)
+    Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor)
 
 public:
-        Toggle(QWidget *parent = nullptr);
+    Toggle(QWidget *parent = nullptr);
 
-        void setState(bool isEnabled);
+    void setState(bool isEnabled);
 
-        void setActiveColor(const QColor &color);
-        void setDisabledColor(const QColor &color);
-        void setInactiveColor(const QColor &color);
-        void setTrackColor(const QColor &color);
+    void setActiveColor(const QColor &color);
+    void setDisabledColor(const QColor &color);
+    void setInactiveColor(const QColor &color);
+    void setTrackColor(const QColor &color);
 
-        QColor activeColor() const { return activeColor_; };
-        QColor disabledColor() const { return disabledColor_; };
-        QColor inactiveColor() const { return inactiveColor_; };
-        QColor trackColor() const { return trackColor_.isValid() ? trackColor_ : QColor("#eee"); };
+    QColor activeColor() const { return activeColor_; };
+    QColor disabledColor() const { return disabledColor_; };
+    QColor inactiveColor() const { return inactiveColor_; };
+    QColor trackColor() const { return trackColor_.isValid() ? trackColor_ : QColor("#eee"); };
 
-        QSize sizeHint() const override { return QSize(64, 48); };
+    QSize sizeHint() const override { return QSize(64, 48); };
 
 protected:
-        void paintEvent(QPaintEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 private:
-        void init();
-        void setupProperties();
+    void init();
+    void setupProperties();
 
-        ToggleTrack *track_;
-        ToggleThumb *thumb_;
+    ToggleTrack *track_;
+    ToggleThumb *thumb_;
 
-        QColor disabledColor_;
-        QColor activeColor_;
-        QColor inactiveColor_;
-        QColor trackColor_;
+    QColor disabledColor_;
+    QColor activeColor_;
+    QColor inactiveColor_;
+    QColor trackColor_;
 };
 
 class ToggleThumb : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor thumbColor WRITE setThumbColor READ thumbColor)
+    Q_PROPERTY(QColor thumbColor WRITE setThumbColor READ thumbColor)
 
 public:
-        ToggleThumb(Toggle *parent);
+    ToggleThumb(Toggle *parent);
 
-        Position shift() const { return position_; };
-        qreal offset() const { return offset_; };
-        QColor thumbColor() const { return thumbColor_; };
+    Position shift() const { return position_; };
+    qreal offset() const { return offset_; };
+    QColor thumbColor() const { return thumbColor_; };
 
-        void setShift(Position position);
-        void setThumbColor(const QColor &color)
-        {
-                thumbColor_ = color;
-                update();
-        };
+    void setShift(Position position);
+    void setThumbColor(const QColor &color)
+    {
+        thumbColor_ = color;
+        update();
+    };
 
 protected:
-        bool eventFilter(QObject *obj, QEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
+    bool eventFilter(QObject *obj, QEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 private:
-        void updateOffset();
+    void updateOffset();
 
-        Toggle *const toggle_;
-        QColor thumbColor_;
+    Toggle *const toggle_;
+    QColor thumbColor_;
 
-        Position position_;
-        qreal offset_;
+    Position position_;
+    qreal offset_;
 };
 
 class ToggleTrack : public QWidget
 {
-        Q_OBJECT
+    Q_OBJECT
 
-        Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor)
+    Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor)
 
 public:
-        ToggleTrack(Toggle *parent);
+    ToggleTrack(Toggle *parent);
 
-        void setTrackColor(const QColor &color);
-        QColor trackColor() const { return trackColor_; };
+    void setTrackColor(const QColor &color);
+    QColor trackColor() const { return trackColor_; };
 
 protected:
-        bool eventFilter(QObject *obj, QEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
+    bool eventFilter(QObject *obj, QEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
 
 private:
-        Toggle *const toggle_;
-        QColor trackColor_;
+    Toggle *const toggle_;
+    QColor trackColor_;
 };
diff --git a/src/ui/UIA.cpp b/src/ui/UIA.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c157ea0f7f4e3a07020352ecb26f1e84fca89c0a
--- /dev/null
+++ b/src/ui/UIA.cpp
@@ -0,0 +1,272 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "UIA.h"
+
+#include <algorithm>
+
+#include <QInputDialog>
+#include <QTimer>
+
+#include <mtx/responses/common.hpp>
+
+#include "Logging.h"
+#include "MainWindow.h"
+#include "dialogs/FallbackAuth.h"
+#include "dialogs/ReCaptcha.h"
+
+UIA *
+UIA::instance()
+{
+    static UIA uia;
+    return &uia;
+}
+
+mtx::http::UIAHandler
+UIA::genericHandler(QString context)
+{
+    return mtx::http::UIAHandler([this, context](const mtx::http::UIAHandler &h,
+                                                 const mtx::user_interactive::Unauthorized &u) {
+        QTimer::singleShot(0, this, [this, h, u, context]() {
+            this->currentHandler = h;
+            this->currentStatus  = u;
+            this->title_         = context;
+            emit titleChanged();
+
+            std::vector<mtx::user_interactive::Flow> flows = u.flows;
+
+            nhlog::ui()->info("Completed stages: {}", u.completed.size());
+
+            if (!u.completed.empty()) {
+                // Get rid of all flows which don't start with the sequence of
+                // stages that have already been completed.
+                flows.erase(std::remove_if(flows.begin(),
+                                           flows.end(),
+                                           [completed_stages = u.completed](auto flow) {
+                                               if (completed_stages.size() > flow.stages.size())
+                                                   return true;
+                                               for (size_t f = 0; f < completed_stages.size(); f++)
+                                                   if (completed_stages[f] != flow.stages[f])
+                                                       return true;
+                                               return false;
+                                           }),
+                            flows.end());
+            }
+
+            if (flows.empty()) {
+                nhlog::ui()->error("No available registration flows!");
+                emit error(tr("No available registration flows!"));
+                return;
+            }
+
+            auto current_stage = flows.front().stages.at(u.completed.size());
+
+            if (current_stage == mtx::user_interactive::auth_types::password) {
+                emit password();
+            } else if (current_stage == mtx::user_interactive::auth_types::email_identity) {
+                emit email();
+            } else if (current_stage == mtx::user_interactive::auth_types::msisdn) {
+                emit phoneNumber();
+            } else if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
+                auto captchaDialog =
+                  new dialogs::ReCaptcha(QString::fromStdString(u.session), MainWindow::instance());
+                captchaDialog->setWindowTitle(context);
+
+                connect(
+                  captchaDialog, &dialogs::ReCaptcha::confirmation, this, [captchaDialog, h, u]() {
+                      captchaDialog->close();
+                      captchaDialog->deleteLater();
+                      h.next(mtx::user_interactive::Auth{u.session,
+                                                         mtx::user_interactive::auth::Fallback{}});
+                  });
+
+                connect(captchaDialog, &dialogs::ReCaptcha::cancel, this, [this]() {
+                    emit error(tr("Registration aborted"));
+                });
+
+                QTimer::singleShot(0, this, [captchaDialog]() { captchaDialog->show(); });
+
+            } else if (current_stage == mtx::user_interactive::auth_types::dummy) {
+                h.next(
+                  mtx::user_interactive::Auth{u.session, mtx::user_interactive::auth::Dummy{}});
+
+            } else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
+                bool ok;
+                QString token =
+                  QInputDialog::getText(MainWindow::instance(),
+                                        context,
+                                        tr("Please enter a valid registration token."),
+                                        QLineEdit::Normal,
+                                        QString(),
+                                        &ok);
+
+                if (ok) {
+                    h.next(mtx::user_interactive::Auth{
+                      u.session,
+                      mtx::user_interactive::auth::RegistrationToken{token.toStdString()}});
+                } else {
+                    emit error(tr("Registration aborted"));
+                }
+            } else {
+                // use fallback
+                auto dialog = new dialogs::FallbackAuth(QString::fromStdString(current_stage),
+                                                        QString::fromStdString(u.session),
+                                                        MainWindow::instance());
+                dialog->setWindowTitle(context);
+
+                connect(dialog, &dialogs::FallbackAuth::confirmation, this, [h, u, dialog]() {
+                    dialog->close();
+                    dialog->deleteLater();
+                    h.next(mtx::user_interactive::Auth{u.session,
+                                                       mtx::user_interactive::auth::Fallback{}});
+                });
+
+                connect(dialog, &dialogs::FallbackAuth::cancel, this, [this]() {
+                    emit error(tr("Registration aborted"));
+                });
+
+                dialog->show();
+            }
+        });
+    });
+}
+
+void
+UIA::continuePassword(QString password)
+{
+    mtx::user_interactive::auth::Password p{};
+    p.identifier_type = mtx::user_interactive::auth::Password::UserId;
+    p.password        = password.toStdString();
+    p.identifier_user = http::client()->user_id().to_string();
+
+    if (currentHandler)
+        currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, p});
+}
+
+void
+UIA::continueEmail(QString email)
+{
+    mtx::requests::RequestEmailToken r{};
+    r.client_secret = this->client_secret = mtx::client::utils::random_token(128, false);
+    r.email                               = email.toStdString();
+    r.send_attempt                        = 0;
+    http::client()->register_email_request_token(
+      r, [this](const mtx::responses::RequestToken &token, mtx::http::RequestErr e) {
+          if (!e) {
+              this->sid        = token.sid;
+              this->submit_url = token.submit_url;
+              this->email_     = true;
+
+              if (submit_url.empty()) {
+                  nhlog::ui()->debug("Got no submit url.");
+                  emit confirm3pidToken();
+              } else {
+                  nhlog::ui()->debug("Got submit url: {}", token.submit_url);
+                  emit prompt3pidToken();
+              }
+          } else {
+              nhlog::ui()->debug("Registering email failed! ({},{},{},{})",
+                                 e->status_code,
+                                 e->status_code,
+                                 e->parse_error,
+                                 e->matrix_error.error);
+              emit error(QString::fromStdString(e->matrix_error.error));
+          }
+      });
+}
+void
+UIA::continuePhoneNumber(QString countryCode, QString phoneNumber)
+{
+    mtx::requests::RequestMSISDNToken r{};
+    r.client_secret = this->client_secret = mtx::client::utils::random_token(128, false);
+    r.country                             = countryCode.toStdString();
+    r.phone_number                        = phoneNumber.toStdString();
+    r.send_attempt                        = 0;
+    http::client()->register_phone_request_token(
+      r, [this](const mtx::responses::RequestToken &token, mtx::http::RequestErr e) {
+          if (!e) {
+              this->sid        = token.sid;
+              this->submit_url = token.submit_url;
+              this->email_     = false;
+              if (submit_url.empty()) {
+                  nhlog::ui()->debug("Got no submit url.");
+                  emit confirm3pidToken();
+              } else {
+                  nhlog::ui()->debug("Got submit url: {}", token.submit_url);
+                  emit prompt3pidToken();
+              }
+          } else {
+              nhlog::ui()->debug("Registering phone number failed! ({},{},{},{})",
+                                 e->status_code,
+                                 e->status_code,
+                                 e->parse_error,
+                                 e->matrix_error.error);
+              emit error(QString::fromStdString(e->matrix_error.error));
+          }
+      });
+}
+
+void
+UIA::continue3pidReceived()
+{
+    mtx::user_interactive::auth::ThreePIDCred c{};
+    c.client_secret = this->client_secret;
+    c.sid           = this->sid;
+
+    if (this->email_) {
+        mtx::user_interactive::auth::EmailIdentity i{};
+        i.threepidCred = c;
+        this->currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, i});
+    } else {
+        mtx::user_interactive::auth::MSISDN i{};
+        i.threepidCred = c;
+        this->currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, i});
+    }
+}
+
+void
+UIA::submit3pidToken(QString token)
+{
+    mtx::requests::IdentitySubmitToken t{};
+    t.client_secret = this->client_secret;
+    t.sid           = this->sid;
+    t.token         = token.toStdString();
+
+    http::client()->validate_submit_token(
+      submit_url, t, [this](const mtx::responses::Success &success, mtx::http::RequestErr e) {
+          if (!e && success.success) {
+              mtx::user_interactive::auth::ThreePIDCred c{};
+              c.client_secret = this->client_secret;
+              c.sid           = this->sid;
+
+              nhlog::ui()->debug("Submit token success");
+
+              if (this->email_) {
+                  mtx::user_interactive::auth::EmailIdentity i{};
+                  i.threepidCred = c;
+                  this->currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, i});
+              } else {
+                  mtx::user_interactive::auth::MSISDN i{};
+                  i.threepidCred = c;
+                  this->currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, i});
+              }
+          } else {
+              if (e) {
+                  nhlog::ui()->debug("Submit token invalid! ({},{},{},{})",
+                                     e->status_code,
+                                     e->status_code,
+                                     e->parse_error,
+                                     e->matrix_error.error);
+                  emit error(QString::fromStdString(e->matrix_error.error));
+              } else {
+                  nhlog::ui()->debug("Submit token invalid!");
+                  emit error(tr("Invalid token"));
+              }
+          }
+
+          this->client_secret.clear();
+          this->sid.clear();
+          this->submit_url.clear();
+      });
+}
diff --git a/src/ui/UIA.h b/src/ui/UIA.h
new file mode 100644
index 0000000000000000000000000000000000000000..0db238975e90a7f2553e3429b3c0dabf097d0aae
--- /dev/null
+++ b/src/ui/UIA.h
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+
+#include <MatrixClient.h>
+
+class UIA : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString title READ title NOTIFY titleChanged)
+
+public:
+    static UIA *instance();
+
+    UIA(QObject *parent = nullptr)
+      : QObject(parent)
+    {}
+
+    mtx::http::UIAHandler genericHandler(QString context);
+
+    QString title() const { return title_; }
+
+public slots:
+    void continuePassword(QString password);
+    void continueEmail(QString email);
+    void continuePhoneNumber(QString countryCode, QString phoneNumber);
+    void submit3pidToken(QString token);
+    void continue3pidReceived();
+
+signals:
+    void password();
+    void email();
+    void phoneNumber();
+
+    void confirm3pidToken();
+    void prompt3pidToken();
+    void tokenAccepted();
+
+    void titleChanged();
+    void error(QString msg);
+
+private:
+    std::optional<mtx::http::UIAHandler> currentHandler;
+    mtx::user_interactive::Unauthorized currentStatus;
+    QString title_;
+
+    // for 3pids like email and phone number
+    std::string client_secret;
+    std::string sid;
+    std::string submit_url;
+    bool email_ = true;
+};
diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index 3d9c4b6a3e4710b013f26813cc4a67744dfdc52c..b5a16f43b9ee0fc617318a816f98a19d7cb07d42 100644
--- a/src/ui/UserProfile.cpp
+++ b/src/ui/UserProfile.cpp
@@ -9,13 +9,14 @@
 
 #include "Cache_p.h"
 #include "ChatPage.h"
-#include "DeviceVerificationFlow.h"
 #include "Logging.h"
 #include "UserProfile.h"
 #include "Utils.h"
+#include "encryption/DeviceVerificationFlow.h"
 #include "mtx/responses/crypto.hpp"
 #include "timeline/TimelineModel.h"
 #include "timeline/TimelineViewManager.h"
+#include "ui/UIA.h"
 
 UserProfile::UserProfile(QString roomid,
                          QString userid,
@@ -27,210 +28,276 @@ UserProfile::UserProfile(QString roomid,
   , manager(manager_)
   , model(parent)
 {
-        globalAvatarUrl = "";
-
-        connect(this,
-                &UserProfile::globalUsernameRetrieved,
-                this,
-                &UserProfile::setGlobalUsername,
-                Qt::QueuedConnection);
-
-        if (isGlobalUserProfile()) {
-                getGlobalProfileData();
-        }
-
-        if (!cache::client() || !cache::client()->isDatabaseReady() ||
-            !ChatPage::instance()->timelineManager())
-                return;
-
-        connect(cache::client(),
-                &Cache::verificationStatusChanged,
-                this,
-                [this](const std::string &user_id) {
-                        if (user_id != this->userid_.toStdString())
-                                return;
-
-                        auto status = cache::verificationStatus(user_id);
-                        if (!status)
-                                return;
-                        this->isUserVerified = status->user_verified;
-                        emit userStatusChanged();
-
-                        for (auto &deviceInfo : deviceList_.deviceList_) {
-                                deviceInfo.verification_status =
-                                  std::find(status->verified_devices.begin(),
-                                            status->verified_devices.end(),
-                                            deviceInfo.device_id.toStdString()) ==
-                                      status->verified_devices.end()
-                                    ? verification::UNVERIFIED
-                                    : verification::VERIFIED;
-                        }
-                        deviceList_.reset(deviceList_.deviceList_);
-                        emit devicesChanged();
-                });
-        fetchDeviceList(this->userid_);
+    globalAvatarUrl = "";
+
+    connect(this,
+            &UserProfile::globalUsernameRetrieved,
+            this,
+            &UserProfile::setGlobalUsername,
+            Qt::QueuedConnection);
+    connect(this, &UserProfile::verificationStatiChanged, &UserProfile::updateVerificationStatus);
+
+    if (isGlobalUserProfile()) {
+        getGlobalProfileData();
+    }
+
+    if (!cache::client() || !cache::client()->isDatabaseReady() ||
+        !ChatPage::instance()->timelineManager())
+        return;
+
+    connect(
+      cache::client(), &Cache::verificationStatusChanged, this, [this](const std::string &user_id) {
+          if (user_id != this->userid_.toStdString())
+              return;
+
+          emit verificationStatiChanged();
+      });
+    fetchDeviceList(this->userid_);
 }
 
 QHash<int, QByteArray>
 DeviceInfoModel::roleNames() const
 {
-        return {
-          {DeviceId, "deviceId"},
-          {DeviceName, "deviceName"},
-          {VerificationStatus, "verificationStatus"},
-        };
+    return {
+      {DeviceId, "deviceId"},
+      {DeviceName, "deviceName"},
+      {VerificationStatus, "verificationStatus"},
+      {LastIp, "lastIp"},
+      {LastTs, "lastTs"},
+    };
 }
 
 QVariant
 DeviceInfoModel::data(const QModelIndex &index, int role) const
 {
-        if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0)
-                return {};
-
-        switch (role) {
-        case DeviceId:
-                return deviceList_[index.row()].device_id;
-        case DeviceName:
-                return deviceList_[index.row()].display_name;
-        case VerificationStatus:
-                return QVariant::fromValue(deviceList_[index.row()].verification_status);
-        default:
-                return {};
-        }
+    if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0)
+        return {};
+
+    switch (role) {
+    case DeviceId:
+        return deviceList_[index.row()].device_id;
+    case DeviceName:
+        return deviceList_[index.row()].display_name;
+    case VerificationStatus:
+        return QVariant::fromValue(deviceList_[index.row()].verification_status);
+    case LastIp:
+        return deviceList_[index.row()].lastIp;
+    case LastTs:
+        return deviceList_[index.row()].lastTs;
+    default:
+        return {};
+    }
 }
 
 void
 DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
 {
-        beginResetModel();
-        this->deviceList_ = std::move(deviceList);
-        endResetModel();
+    beginResetModel();
+    this->deviceList_ = std::move(deviceList);
+    endResetModel();
 }
 
 DeviceInfoModel *
 UserProfile::deviceList()
 {
-        return &this->deviceList_;
+    return &this->deviceList_;
 }
 
 QString
 UserProfile::userid()
 {
-        return this->userid_;
+    return this->userid_;
 }
 
 QString
 UserProfile::displayName()
 {
-        return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_);
+    return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_);
 }
 
 QString
 UserProfile::avatarUrl()
 {
-        return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
+    return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
 }
 
 bool
 UserProfile::isGlobalUserProfile() const
 {
-        return roomid_ == "";
+    return roomid_ == "";
 }
 
 crypto::Trust
 UserProfile::getUserStatus()
 {
-        return isUserVerified;
+    return isUserVerified;
 }
 
 bool
 UserProfile::userVerificationEnabled() const
 {
-        return hasMasterKey;
+    return hasMasterKey;
 }
 bool
 UserProfile::isSelf() const
 {
-        return this->userid_ == utils::localUser();
+    return this->userid_ == utils::localUser();
 }
 
 void
-UserProfile::fetchDeviceList(const QString &userID)
+UserProfile::signOutDevice(const QString &deviceID)
 {
-        auto localUser = utils::localUser();
+    http::client()->delete_device(
+      deviceID.toStdString(),
+      UIA::instance()->genericHandler(tr("Sign out device %1").arg(deviceID)),
+      [this, deviceID](mtx::http::RequestErr e) {
+          if (e) {
+              nhlog::ui()->critical("Failure when attempting to sign out device {}",
+                                    deviceID.toStdString());
+              return;
+          }
+          nhlog::ui()->info("Device {} successfully signed out!", deviceID.toStdString());
+          // This is us. Let's update the interface accordingly
+          if (isSelf() && deviceID.toStdString() == ::http::client()->device_id()) {
+              ChatPage::instance()->dropToLoginPageCb(tr("You signed out this device."));
+          }
+          refreshDevices();
+      });
+}
 
-        if (!cache::client() || !cache::client()->isDatabaseReady())
-                return;
+void
+UserProfile::refreshDevices()
+{
+    cache::client()->markUserKeysOutOfDate({this->userid_.toStdString()});
+    fetchDeviceList(this->userid_);
+}
 
-        cache::client()->query_keys(
-          userID.toStdString(),
-          [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys,
-                                                       mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to query device keys: {},{}",
-                                             mtx::errors::to_string(err->matrix_error.errcode),
-                                             static_cast<int>(err->status_code));
-                          return;
+void
+UserProfile::fetchDeviceList(const QString &userID)
+{
+    auto localUser = utils::localUser();
+
+    if (!cache::client() || !cache::client()->isDatabaseReady())
+        return;
+
+    cache::client()->query_keys(
+      userID.toStdString(),
+      [other_user_id = userID.toStdString(), this](const UserKeyCache &,
+                                                   mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to query device keys: {},{}",
+                                 mtx::errors::to_string(err->matrix_error.errcode),
+                                 static_cast<int>(err->status_code));
+          }
+
+          // Ensure local key cache is up to date
+          cache::client()->query_keys(
+            utils::localUser().toStdString(),
+            [this](const UserKeyCache &, mtx::http::RequestErr err) {
+                using namespace mtx;
+                std::string local_user_id = utils::localUser().toStdString();
+
+                if (err) {
+                    nhlog::net()->warn("failed to query device keys: {},{}",
+                                       mtx::errors::to_string(err->matrix_error.errcode),
+                                       static_cast<int>(err->status_code));
+                }
+
+                emit verificationStatiChanged();
+            });
+      });
+}
+
+void
+UserProfile::updateVerificationStatus()
+{
+    if (!cache::client() || !cache::client()->isDatabaseReady())
+        return;
+
+    auto user_keys = cache::client()->userKeys(userid_.toStdString());
+    if (!user_keys) {
+        this->hasMasterKey   = false;
+        this->isUserVerified = crypto::Trust::Unverified;
+        this->deviceList_.reset({});
+        emit userStatusChanged();
+        return;
+    }
+
+    this->hasMasterKey = !user_keys->master_keys.keys.empty();
+
+    std::vector<DeviceInfo> deviceInfo;
+    auto devices            = user_keys->device_keys;
+    auto verificationStatus = cache::client()->verificationStatus(userid_.toStdString());
+
+    this->isUserVerified = verificationStatus.user_verified;
+    emit userStatusChanged();
+
+    for (const auto &d : devices) {
+        auto device = d.second;
+        verification::Status verified =
+          std::find(verificationStatus.verified_devices.begin(),
+                    verificationStatus.verified_devices.end(),
+                    device.device_id) == verificationStatus.verified_devices.end()
+            ? verification::UNVERIFIED
+            : verification::VERIFIED;
+
+        if (isSelf() && device.device_id == ::http::client()->device_id())
+            verified = verification::Status::SELF;
+
+        deviceInfo.push_back({QString::fromStdString(d.first),
+                              QString::fromStdString(device.unsigned_info.device_display_name),
+                              verified});
+    }
+
+    // For self, also query devices without keys
+    if (isSelf()) {
+        http::client()->query_devices(
+          [this, deviceInfo](const mtx::responses::QueryDevices &allDevs,
+                             mtx::http::RequestErr err) mutable {
+              if (err) {
+                  nhlog::net()->warn("failed to query devices: {} {}",
+                                     err->matrix_error.error,
+                                     static_cast<int>(err->status_code));
+                  this->deviceList_.queueReset(std::move(deviceInfo));
+                  emit devicesChanged();
+                  return;
+              }
+              for (const auto &d : allDevs.devices) {
+                  // First, check if we already have an entry for this device
+                  bool found = false;
+                  for (auto &e : deviceInfo) {
+                      if (e.device_id.toStdString() == d.device_id) {
+                          found = true;
+                          // Gottem! Let's fill in the blanks
+                          e.lastIp = QString::fromStdString(d.last_seen_ip);
+                          e.lastTs = d.last_seen_ts;
+                          break;
+                      }
                   }
+                  // No entry? Let's add one.
+                  if (!found) {
+                      deviceInfo.push_back({QString::fromStdString(d.device_id),
+                                            QString::fromStdString(d.display_name),
+                                            verification::NOT_APPLICABLE,
+                                            QString::fromStdString(d.last_seen_ip),
+                                            d.last_seen_ts});
+                  }
+              }
 
-                  // Ensure local key cache is up to date
-                  cache::client()->query_keys(
-                    utils::localUser().toStdString(),
-                    [other_user_id, other_user_keys, this](const UserKeyCache &,
-                                                           mtx::http::RequestErr err) {
-                            using namespace mtx;
-                            std::string local_user_id = utils::localUser().toStdString();
-
-                            if (err) {
-                                    nhlog::net()->warn(
-                                      "failed to query device keys: {},{}",
-                                      mtx::errors::to_string(err->matrix_error.errcode),
-                                      static_cast<int>(err->status_code));
-                                    return;
-                            }
-
-                            this->hasMasterKey = !other_user_keys.master_keys.keys.empty();
-
-                            std::vector<DeviceInfo> deviceInfo;
-                            auto devices = other_user_keys.device_keys;
-                            auto verificationStatus =
-                              cache::client()->verificationStatus(other_user_id);
-
-                            isUserVerified = verificationStatus.user_verified;
-                            emit userStatusChanged();
-
-                            for (const auto &d : devices) {
-                                    auto device = d.second;
-                                    verification::Status verified =
-                                      verification::Status::UNVERIFIED;
-
-                                    if (std::find(verificationStatus.verified_devices.begin(),
-                                                  verificationStatus.verified_devices.end(),
-                                                  device.device_id) !=
-                                          verificationStatus.verified_devices.end() &&
-                                        mtx::crypto::verify_identity_signature(
-                                          device,
-                                          DeviceId(device.device_id),
-                                          UserId(other_user_id)))
-                                            verified = verification::Status::VERIFIED;
-
-                                    deviceInfo.push_back(
-                                      {QString::fromStdString(d.first),
-                                       QString::fromStdString(
-                                         device.unsigned_info.device_display_name),
-                                       verified});
-                            }
-
-                            this->deviceList_.queueReset(std::move(deviceInfo));
-                            emit devicesChanged();
-                    });
+              this->deviceList_.queueReset(std::move(deviceInfo));
+              emit devicesChanged();
           });
+        return;
+    }
+
+    this->deviceList_.queueReset(std::move(deviceInfo));
+    emit devicesChanged();
 }
 
 void
 UserProfile::banUser()
 {
-        ChatPage::instance()->banUser(this->userid_, "");
+    ChatPage::instance()->banUser(this->userid_, "");
 }
 
 // void ignoreUser(){
@@ -240,181 +307,193 @@ UserProfile::banUser()
 void
 UserProfile::kickUser()
 {
-        ChatPage::instance()->kickUser(this->userid_, "");
+    ChatPage::instance()->kickUser(this->userid_, "");
 }
 
 void
 UserProfile::startChat()
 {
-        ChatPage::instance()->startChat(this->userid_);
+    ChatPage::instance()->startChat(this->userid_);
 }
 
 void
 UserProfile::changeUsername(QString username)
 {
-        if (isGlobalUserProfile()) {
-                // change global
-                http::client()->set_displayname(
-                  username.toStdString(), [](mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::net()->warn("could not change username");
-                                  return;
-                          }
-                  });
-        } else {
-                // change room username
-                mtx::events::state::Member member;
-                member.display_name = username.toStdString();
-                member.avatar_url =
-                  cache::avatarUrl(roomid_,
-                                   QString::fromStdString(http::client()->user_id().to_string()))
-                    .toStdString();
-                member.membership = mtx::events::state::Membership::Join;
-
-                updateRoomMemberState(std::move(member));
-        }
+    if (isGlobalUserProfile()) {
+        // change global
+        http::client()->set_displayname(username.toStdString(), [](mtx::http::RequestErr err) {
+            if (err) {
+                nhlog::net()->warn("could not change username");
+                return;
+            }
+        });
+    } else {
+        // change room username
+        mtx::events::state::Member member;
+        member.display_name = username.toStdString();
+        member.avatar_url =
+          cache::avatarUrl(roomid_, QString::fromStdString(http::client()->user_id().to_string()))
+            .toStdString();
+        member.membership = mtx::events::state::Membership::Join;
+
+        updateRoomMemberState(std::move(member));
+    }
+}
+
+void
+UserProfile::changeDeviceName(QString deviceID, QString deviceName)
+{
+    http::client()->set_device_name(
+      deviceID.toStdString(), deviceName.toStdString(), [this](mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("could not change device name");
+              return;
+          }
+          refreshDevices();
+      });
 }
 
 void
 UserProfile::verify(QString device)
 {
-        if (!device.isEmpty())
-                manager->verifyDevice(userid_, device);
-        else {
-                manager->verifyUser(userid_);
-        }
+    if (!device.isEmpty())
+        manager->verificationManager()->verifyDevice(userid_, device);
+    else {
+        manager->verificationManager()->verifyUser(userid_);
+    }
 }
 
 void
 UserProfile::unverify(QString device)
 {
-        cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
+    cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
 }
 
 void
 UserProfile::setGlobalUsername(const QString &globalUser)
 {
-        globalUsername = globalUser;
-        emit displayNameChanged();
+    globalUsername = globalUser;
+    emit displayNameChanged();
 }
 
 void
 UserProfile::changeAvatar()
 {
-        const QString picturesFolder =
-          QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
-        const QString fileName = QFileDialog::getOpenFileName(
-          nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
-
-        if (fileName.isEmpty())
-                return;
-
-        QMimeDatabase db;
-        QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
-
-        const auto format = mime.name().split("/")[0];
-
-        QFile file{fileName, this};
-        if (format != "image") {
-                emit displayError(tr("The selected file is not an image"));
-                return;
-        }
-
-        if (!file.open(QIODevice::ReadOnly)) {
-                emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
-                return;
-        }
-
-        const auto bin     = file.peek(file.size());
-        const auto payload = std::string(bin.data(), bin.size());
-
-        isLoading_ = true;
-        emit loadingChanged();
-
-        // First we need to create a new mxc URI
-        // (i.e upload media to the Matrix content repository) for the new avatar.
-        http::client()->upload(
-          payload,
-          mime.name().toStdString(),
-          QFileInfo(fileName).fileName().toStdString(),
-          [this,
-           payload,
-           mimetype = mime.name().toStdString(),
-           size     = payload.size(),
-           room_id  = roomid_.toStdString(),
-           content  = std::move(bin)](const mtx::responses::ContentURI &res,
-                                     mtx::http::RequestErr err) {
+    const QString picturesFolder =
+      QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
+    const QString fileName = QFileDialog::getOpenFileName(
+      nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
+
+    if (fileName.isEmpty())
+        return;
+
+    QMimeDatabase db;
+    QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
+
+    const auto format = mime.name().split("/")[0];
+
+    QFile file{fileName, this};
+    if (format != "image") {
+        emit displayError(tr("The selected file is not an image"));
+        return;
+    }
+
+    if (!file.open(QIODevice::ReadOnly)) {
+        emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
+        return;
+    }
+
+    const auto bin     = file.peek(file.size());
+    const auto payload = std::string(bin.data(), bin.size());
+
+    isLoading_ = true;
+    emit loadingChanged();
+
+    // First we need to create a new mxc URI
+    // (i.e upload media to the Matrix content repository) for the new avatar.
+    http::client()->upload(
+      payload,
+      mime.name().toStdString(),
+      QFileInfo(fileName).fileName().toStdString(),
+      [this,
+       payload,
+       mimetype = mime.name().toStdString(),
+       size     = payload.size(),
+       room_id  = roomid_.toStdString(),
+       content = std::move(bin)](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::ui()->error("Failed to upload image", err->matrix_error.error);
+              return;
+          }
+
+          if (isGlobalUserProfile()) {
+              http::client()->set_avatar_url(res.content_uri, [this](mtx::http::RequestErr err) {
                   if (err) {
-                          nhlog::ui()->error("Failed to upload image", err->matrix_error.error);
-                          return;
+                      nhlog::ui()->error("Failed to set user avatar url", err->matrix_error.error);
                   }
 
-                  if (isGlobalUserProfile()) {
-                          http::client()->set_avatar_url(
-                            res.content_uri, [this](mtx::http::RequestErr err) {
-                                    if (err) {
-                                            nhlog::ui()->error("Failed to set user avatar url",
-                                                               err->matrix_error.error);
-                                    }
-
-                                    isLoading_ = false;
-                                    emit loadingChanged();
-                                    getGlobalProfileData();
-                            });
-                  } else {
-                          // change room username
-                          mtx::events::state::Member member;
-                          member.display_name = cache::displayName(roomid_, userid_).toStdString();
-                          member.avatar_url   = res.content_uri;
-                          member.membership   = mtx::events::state::Membership::Join;
-
-                          updateRoomMemberState(std::move(member));
-                  }
-          });
+                  isLoading_ = false;
+                  emit loadingChanged();
+                  getGlobalProfileData();
+              });
+          } else {
+              // change room username
+              mtx::events::state::Member member;
+              member.display_name = cache::displayName(roomid_, userid_).toStdString();
+              member.avatar_url   = res.content_uri;
+              member.membership   = mtx::events::state::Membership::Join;
+
+              updateRoomMemberState(std::move(member));
+          }
+      });
 }
 
 void
 UserProfile::updateRoomMemberState(mtx::events::state::Member member)
 {
-        http::client()->send_state_event(roomid_.toStdString(),
-                                         http::client()->user_id().to_string(),
-                                         member,
-                                         [](mtx::responses::EventId, mtx::http::RequestErr err) {
-                                                 if (err)
-                                                         nhlog::net()->error(
-                                                           "Failed to update room member state : ",
-                                                           err->matrix_error.error);
-                                         });
+    http::client()->send_state_event(
+      roomid_.toStdString(),
+      http::client()->user_id().to_string(),
+      member,
+      [](mtx::responses::EventId, mtx::http::RequestErr err) {
+          if (err)
+              nhlog::net()->error("Failed to update room member state : ", err->matrix_error.error);
+      });
 }
 
 void
 UserProfile::updateAvatarUrl()
 {
-        isLoading_ = false;
-        emit loadingChanged();
+    isLoading_ = false;
+    emit loadingChanged();
 
-        emit avatarUrlChanged();
+    emit avatarUrlChanged();
 }
 
 bool
 UserProfile::isLoading() const
 {
-        return isLoading_;
+    return isLoading_;
 }
 
 void
 UserProfile::getGlobalProfileData()
 {
-        http::client()->get_profile(
-          userid_.toStdString(),
-          [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to retrieve own profile info");
-                          return;
-                  }
+    http::client()->get_profile(
+      userid_.toStdString(), [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to retrieve profile info for {}", userid_.toStdString());
+              return;
+          }
+
+          emit globalUsernameRetrieved(QString::fromStdString(res.display_name));
+          globalAvatarUrl = QString::fromStdString(res.avatar_url);
+          emit avatarUrlChanged();
+      });
+}
 
-                  emit globalUsernameRetrieved(QString::fromStdString(res.display_name));
-                  globalAvatarUrl = QString::fromStdString(res.avatar_url);
-                  emit avatarUrlChanged();
-          });
+void
+UserProfile::openGlobalProfile()
+{
+    emit manager->openGlobalUserProfile(userid_);
 }
diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h
index 721d72301d17d7ecd07c376fc0e898bd3a8527ed..cd2f474005fbfa68479bffd8c1fc44f4645c47ec 100644
--- a/src/ui/UserProfile.h
+++ b/src/ui/UserProfile.h
@@ -18,9 +18,11 @@ Q_NAMESPACE
 
 enum Status
 {
-        VERIFIED,
-        UNVERIFIED,
-        BLOCKED
+    SELF,
+    VERIFIED,
+    UNVERIFIED,
+    BLOCKED,
+    NOT_APPLICABLE
 };
 Q_ENUM_NS(Status)
 }
@@ -32,127 +34,150 @@ class TimelineViewManager;
 class DeviceInfo
 {
 public:
-        DeviceInfo(const QString deviceID,
-                   const QString displayName,
-                   verification::Status verification_status_)
-          : device_id(deviceID)
-          , display_name(displayName)
-          , verification_status(verification_status_)
-        {}
-        DeviceInfo()
-          : verification_status(verification::UNVERIFIED)
-        {}
-
-        QString device_id;
-        QString display_name;
-
-        verification::Status verification_status;
+    DeviceInfo(const QString deviceID,
+               const QString displayName,
+               verification::Status verification_status_,
+               const QString lastIp_,
+               const size_t lastTs_)
+      : device_id(deviceID)
+      , display_name(displayName)
+      , verification_status(verification_status_)
+      , lastIp(lastIp_)
+      , lastTs(lastTs_)
+    {}
+    DeviceInfo(const QString deviceID,
+               const QString displayName,
+               verification::Status verification_status_)
+      : device_id(deviceID)
+      , display_name(displayName)
+      , verification_status(verification_status_)
+      , lastTs(0)
+    {}
+    DeviceInfo()
+      : verification_status(verification::UNVERIFIED)
+    {}
+
+    QString device_id;
+    QString display_name;
+
+    verification::Status verification_status;
+    QString lastIp;
+    qlonglong lastTs;
 };
 
 class DeviceInfoModel : public QAbstractListModel
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        enum Roles
-        {
-                DeviceId,
-                DeviceName,
-                VerificationStatus,
-        };
-
-        explicit DeviceInfoModel(QObject *parent = nullptr)
-        {
-                (void)parent;
-                connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset);
-        };
-        QHash<int, QByteArray> roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override
-        {
-                (void)parent;
-                return (int)deviceList_.size();
-        }
-        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+    enum Roles
+    {
+        DeviceId,
+        DeviceName,
+        VerificationStatus,
+        LastIp,
+        LastTs,
+    };
+
+    explicit DeviceInfoModel(QObject *parent = nullptr)
+    {
+        (void)parent;
+        connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset);
+    };
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        (void)parent;
+        return (int)deviceList_.size();
+    }
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
 
 signals:
-        void queueReset(const std::vector<DeviceInfo> &deviceList);
+    void queueReset(const std::vector<DeviceInfo> &deviceList);
 public slots:
-        void reset(const std::vector<DeviceInfo> &deviceList);
+    void reset(const std::vector<DeviceInfo> &deviceList);
 
 private:
-        std::vector<DeviceInfo> deviceList_;
+    std::vector<DeviceInfo> deviceList_;
 
-        friend class UserProfile;
+    friend class UserProfile;
 };
 
 class UserProfile : public QObject
 {
-        Q_OBJECT
-        Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged)
-        Q_PROPERTY(QString userid READ userid CONSTANT)
-        Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
-        Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged)
-        Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT)
-        Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged)
-        Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
-        Q_PROPERTY(
-          bool userVerificationEnabled READ userVerificationEnabled NOTIFY userStatusChanged)
-        Q_PROPERTY(bool isSelf READ isSelf CONSTANT)
-        Q_PROPERTY(TimelineModel *room READ room CONSTANT)
+    Q_OBJECT
+    Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged)
+    Q_PROPERTY(QString userid READ userid CONSTANT)
+    Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
+    Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged)
+    Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT)
+    Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged)
+    Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
+    Q_PROPERTY(bool userVerificationEnabled READ userVerificationEnabled NOTIFY userStatusChanged)
+    Q_PROPERTY(bool isSelf READ isSelf CONSTANT)
+    Q_PROPERTY(TimelineModel *room READ room CONSTANT)
 public:
-        UserProfile(QString roomid,
-                    QString userid,
-                    TimelineViewManager *manager_,
-                    TimelineModel *parent = nullptr);
-
-        DeviceInfoModel *deviceList();
-
-        QString userid();
-        QString displayName();
-        QString avatarUrl();
-        bool isGlobalUserProfile() const;
-        crypto::Trust getUserStatus();
-        bool userVerificationEnabled() const;
-        bool isSelf() const;
-        bool isLoading() const;
-        TimelineModel *room() const { return model; }
-
-        Q_INVOKABLE void verify(QString device = "");
-        Q_INVOKABLE void unverify(QString device = "");
-        Q_INVOKABLE void fetchDeviceList(const QString &userID);
-        Q_INVOKABLE void banUser();
-        // Q_INVOKABLE void ignoreUser();
-        Q_INVOKABLE void kickUser();
-        Q_INVOKABLE void startChat();
-        Q_INVOKABLE void changeUsername(QString username);
-        Q_INVOKABLE void changeAvatar();
+    UserProfile(QString roomid,
+                QString userid,
+                TimelineViewManager *manager_,
+                TimelineModel *parent = nullptr);
+
+    DeviceInfoModel *deviceList();
+
+    QString userid();
+    QString displayName();
+    QString avatarUrl();
+    bool isGlobalUserProfile() const;
+    crypto::Trust getUserStatus();
+    bool userVerificationEnabled() const;
+    bool isSelf() const;
+    bool isLoading() const;
+    TimelineModel *room() const { return model; }
+
+    Q_INVOKABLE void verify(QString device = "");
+    Q_INVOKABLE void unverify(QString device = "");
+    Q_INVOKABLE void fetchDeviceList(const QString &userID);
+    Q_INVOKABLE void refreshDevices();
+    Q_INVOKABLE void banUser();
+    Q_INVOKABLE void signOutDevice(const QString &deviceID);
+    // Q_INVOKABLE void ignoreUser();
+    Q_INVOKABLE void kickUser();
+    Q_INVOKABLE void startChat();
+    Q_INVOKABLE void changeUsername(QString username);
+    Q_INVOKABLE void changeDeviceName(QString deviceID, QString deviceName);
+    Q_INVOKABLE void changeAvatar();
+    Q_INVOKABLE void openGlobalProfile();
 
 signals:
-        void userStatusChanged();
-        void loadingChanged();
-        void displayNameChanged();
-        void avatarUrlChanged();
-        void displayError(const QString &errorMessage);
-        void globalUsernameRetrieved(const QString &globalUser);
-        void devicesChanged();
+    void userStatusChanged();
+    void loadingChanged();
+    void displayNameChanged();
+    void avatarUrlChanged();
+    void displayError(const QString &errorMessage);
+    void globalUsernameRetrieved(const QString &globalUser);
+    void devicesChanged();
+
+    // internal
+    void verificationStatiChanged();
 
 public slots:
-        void updateAvatarUrl();
+    void updateAvatarUrl();
 
-protected slots:
-        void setGlobalUsername(const QString &globalUser);
+private slots:
+    void setGlobalUsername(const QString &globalUser);
+    void updateVerificationStatus();
 
 private:
-        void updateRoomMemberState(mtx::events::state::Member member);
-        void getGlobalProfileData();
+    void updateRoomMemberState(mtx::events::state::Member member);
+    void getGlobalProfileData();
 
 private:
-        QString roomid_, userid_;
-        QString globalUsername;
-        QString globalAvatarUrl;
-        DeviceInfoModel deviceList_;
-        crypto::Trust isUserVerified = crypto::Trust::Unverified;
-        bool hasMasterKey            = false;
-        bool isLoading_              = false;
-        TimelineViewManager *manager;
-        TimelineModel *model;
+    QString roomid_, userid_;
+    QString globalUsername;
+    QString globalAvatarUrl;
+    DeviceInfoModel deviceList_;
+    crypto::Trust isUserVerified = crypto::Trust::Unverified;
+    bool hasMasterKey            = false;
+    bool isLoading_              = false;
+    TimelineViewManager *manager;
+    TimelineModel *model;
 };
diff --git a/src/voip/CallDevices.cpp b/src/voip/CallDevices.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..be185470545e3abb4f45c69636f276fdf9d34302
--- /dev/null
+++ b/src/voip/CallDevices.cpp
@@ -0,0 +1,385 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <cstring>
+#include <optional>
+#include <string_view>
+
+#include "CallDevices.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "UserSettingsPage.h"
+
+#ifdef GSTREAMER_AVAILABLE
+extern "C"
+{
+#include "gst/gst.h"
+}
+#endif
+
+CallDevices::CallDevices()
+  : QObject()
+{}
+
+#ifdef GSTREAMER_AVAILABLE
+namespace {
+
+struct AudioSource
+{
+    std::string name;
+    GstDevice *device;
+};
+
+struct VideoSource
+{
+    struct Caps
+    {
+        std::string resolution;
+        std::vector<std::string> frameRates;
+    };
+    std::string name;
+    GstDevice *device;
+    std::vector<Caps> caps;
+};
+
+std::vector<AudioSource> audioSources_;
+std::vector<VideoSource> videoSources_;
+
+using FrameRate = std::pair<int, int>;
+std::optional<FrameRate>
+getFrameRate(const GValue *value)
+{
+    if (GST_VALUE_HOLDS_FRACTION(value)) {
+        gint num = gst_value_get_fraction_numerator(value);
+        gint den = gst_value_get_fraction_denominator(value);
+        return FrameRate{num, den};
+    }
+    return std::nullopt;
+}
+
+void
+addFrameRate(std::vector<std::string> &rates, const FrameRate &rate)
+{
+    constexpr double minimumFrameRate = 15.0;
+    if (static_cast<double>(rate.first) / rate.second >= minimumFrameRate)
+        rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second));
+}
+
+void
+setDefaultDevice(bool isVideo)
+{
+    auto settings = ChatPage::instance()->userSettings();
+    if (isVideo && settings->camera().isEmpty()) {
+        const VideoSource &camera = videoSources_.front();
+        settings->setCamera(QString::fromStdString(camera.name));
+        settings->setCameraResolution(QString::fromStdString(camera.caps.front().resolution));
+        settings->setCameraFrameRate(
+          QString::fromStdString(camera.caps.front().frameRates.front()));
+    } else if (!isVideo && settings->microphone().isEmpty()) {
+        settings->setMicrophone(QString::fromStdString(audioSources_.front().name));
+    }
+}
+
+void
+addDevice(GstDevice *device)
+{
+    if (!device)
+        return;
+
+    gchar *name  = gst_device_get_display_name(device);
+    gchar *type  = gst_device_get_device_class(device);
+    bool isVideo = !std::strncmp(type, "Video", 5);
+    g_free(type);
+    nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name);
+    if (!isVideo) {
+        audioSources_.push_back({name, device});
+        g_free(name);
+        setDefaultDevice(false);
+        return;
+    }
+
+    GstCaps *gstcaps = gst_device_get_caps(device);
+    if (!gstcaps) {
+        nhlog::ui()->debug("WebRTC: unable to get caps for {}", name);
+        g_free(name);
+        return;
+    }
+
+    VideoSource source{name, device, {}};
+    g_free(name);
+    guint nCaps = gst_caps_get_size(gstcaps);
+    for (guint i = 0; i < nCaps; ++i) {
+        GstStructure *structure  = gst_caps_get_structure(gstcaps, i);
+        const gchar *struct_name = gst_structure_get_name(structure);
+        if (!std::strcmp(struct_name, "video/x-raw")) {
+            gint widthpx, heightpx;
+            if (gst_structure_get(structure,
+                                  "width",
+                                  G_TYPE_INT,
+                                  &widthpx,
+                                  "height",
+                                  G_TYPE_INT,
+                                  &heightpx,
+                                  nullptr)) {
+                VideoSource::Caps caps;
+                caps.resolution     = std::to_string(widthpx) + "x" + std::to_string(heightpx);
+                const GValue *value = gst_structure_get_value(structure, "framerate");
+                if (auto fr = getFrameRate(value); fr)
+                    addFrameRate(caps.frameRates, *fr);
+                else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) {
+                    addFrameRate(caps.frameRates,
+                                 *getFrameRate(gst_value_get_fraction_range_min(value)));
+                    addFrameRate(caps.frameRates,
+                                 *getFrameRate(gst_value_get_fraction_range_max(value)));
+                } else if (GST_VALUE_HOLDS_LIST(value)) {
+                    guint nRates = gst_value_list_get_size(value);
+                    for (guint j = 0; j < nRates; ++j) {
+                        const GValue *rate = gst_value_list_get_value(value, j);
+                        if (auto frate = getFrameRate(rate); frate)
+                            addFrameRate(caps.frameRates, *frate);
+                    }
+                }
+                if (!caps.frameRates.empty())
+                    source.caps.push_back(std::move(caps));
+            }
+        }
+    }
+    gst_caps_unref(gstcaps);
+    videoSources_.push_back(std::move(source));
+    setDefaultDevice(true);
+}
+
+template<typename T>
+bool
+removeDevice(T &sources, GstDevice *device, bool changed)
+{
+    if (auto it = std::find_if(
+          sources.begin(), sources.end(), [device](const auto &s) { return s.device == device; });
+        it != sources.end()) {
+        nhlog::ui()->debug(
+          std::string("WebRTC: device ") + (changed ? "changed: " : "removed: ") + "{}", it->name);
+        gst_object_unref(device);
+        sources.erase(it);
+        return true;
+    }
+    return false;
+}
+
+void
+removeDevice(GstDevice *device, bool changed)
+{
+    if (device) {
+        if (removeDevice(audioSources_, device, changed) ||
+            removeDevice(videoSources_, device, changed))
+            return;
+    }
+}
+
+gboolean
+newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data G_GNUC_UNUSED)
+{
+    switch (GST_MESSAGE_TYPE(msg)) {
+    case GST_MESSAGE_DEVICE_ADDED: {
+        GstDevice *device;
+        gst_message_parse_device_added(msg, &device);
+        addDevice(device);
+        emit CallDevices::instance().devicesChanged();
+        break;
+    }
+    case GST_MESSAGE_DEVICE_REMOVED: {
+        GstDevice *device;
+        gst_message_parse_device_removed(msg, &device);
+        removeDevice(device, false);
+        emit CallDevices::instance().devicesChanged();
+        break;
+    }
+    case GST_MESSAGE_DEVICE_CHANGED: {
+        GstDevice *device;
+        GstDevice *oldDevice;
+        gst_message_parse_device_changed(msg, &device, &oldDevice);
+        removeDevice(oldDevice, true);
+        addDevice(device);
+        break;
+    }
+    default:
+        break;
+    }
+    return TRUE;
+}
+
+template<typename T>
+std::vector<std::string>
+deviceNames(T &sources, const std::string &defaultDevice)
+{
+    std::vector<std::string> ret;
+    ret.reserve(sources.size());
+    for (const auto &s : sources)
+        ret.push_back(s.name);
+
+    // move default device to top of the list
+    if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end())
+        std::swap(ret.front(), *it);
+
+    return ret;
+}
+
+std::optional<VideoSource>
+getVideoSource(const std::string &cameraName)
+{
+    if (auto it = std::find_if(videoSources_.cbegin(),
+                               videoSources_.cend(),
+                               [&cameraName](const auto &s) { return s.name == cameraName; });
+        it != videoSources_.cend()) {
+        return *it;
+    }
+    return std::nullopt;
+}
+
+std::pair<int, int>
+tokenise(std::string_view str, char delim)
+{
+    std::pair<int, int> ret;
+    ret.first  = std::atoi(str.data());
+    auto pos   = str.find_first_of(delim);
+    ret.second = std::atoi(str.data() + pos + 1);
+    return ret;
+}
+}
+
+void
+CallDevices::init()
+{
+    static GstDeviceMonitor *monitor = nullptr;
+    if (!monitor) {
+        monitor       = gst_device_monitor_new();
+        GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
+        gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
+        gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps);
+        gst_caps_unref(caps);
+        caps = gst_caps_new_empty_simple("video/x-raw");
+        gst_device_monitor_add_filter(monitor, "Video/Source", caps);
+        gst_device_monitor_add_filter(monitor, "Video/Duplex", caps);
+        gst_caps_unref(caps);
+
+        GstBus *bus = gst_device_monitor_get_bus(monitor);
+        gst_bus_add_watch(bus, newBusMessage, nullptr);
+        gst_object_unref(bus);
+        if (!gst_device_monitor_start(monitor)) {
+            nhlog::ui()->error("WebRTC: failed to start device monitor");
+            return;
+        }
+    }
+}
+
+bool
+CallDevices::haveMic() const
+{
+    return !audioSources_.empty();
+}
+
+bool
+CallDevices::haveCamera() const
+{
+    return !videoSources_.empty();
+}
+
+std::vector<std::string>
+CallDevices::names(bool isVideo, const std::string &defaultDevice) const
+{
+    return isVideo ? deviceNames(videoSources_, defaultDevice)
+                   : deviceNames(audioSources_, defaultDevice);
+}
+
+std::vector<std::string>
+CallDevices::resolutions(const std::string &cameraName) const
+{
+    std::vector<std::string> ret;
+    if (auto s = getVideoSource(cameraName); s) {
+        ret.reserve(s->caps.size());
+        for (const auto &c : s->caps)
+            ret.push_back(c.resolution);
+    }
+    return ret;
+}
+
+std::vector<std::string>
+CallDevices::frameRates(const std::string &cameraName, const std::string &resolution) const
+{
+    if (auto s = getVideoSource(cameraName); s) {
+        if (auto it = std::find_if(s->caps.cbegin(),
+                                   s->caps.cend(),
+                                   [&](const auto &c) { return c.resolution == resolution; });
+            it != s->caps.cend())
+            return it->frameRates;
+    }
+    return {};
+}
+
+GstDevice *
+CallDevices::audioDevice() const
+{
+    std::string name = ChatPage::instance()->userSettings()->microphone().toStdString();
+    if (auto it = std::find_if(audioSources_.cbegin(),
+                               audioSources_.cend(),
+                               [&name](const auto &s) { return s.name == name; });
+        it != audioSources_.cend()) {
+        nhlog::ui()->debug("WebRTC: microphone: {}", name);
+        return it->device;
+    } else {
+        nhlog::ui()->error("WebRTC: unknown microphone: {}", name);
+        return nullptr;
+    }
+}
+
+GstDevice *
+CallDevices::videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const
+{
+    auto settings    = ChatPage::instance()->userSettings();
+    std::string name = settings->camera().toStdString();
+    if (auto s = getVideoSource(name); s) {
+        nhlog::ui()->debug("WebRTC: camera: {}", name);
+        resolution = tokenise(settings->cameraResolution().toStdString(), 'x');
+        frameRate  = tokenise(settings->cameraFrameRate().toStdString(), '/');
+        nhlog::ui()->debug("WebRTC: camera resolution: {}x{}", resolution.first, resolution.second);
+        nhlog::ui()->debug("WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second);
+        return s->device;
+    } else {
+        nhlog::ui()->error("WebRTC: unknown camera: {}", name);
+        return nullptr;
+    }
+}
+
+#else
+
+bool
+CallDevices::haveMic() const
+{
+    return false;
+}
+
+bool
+CallDevices::haveCamera() const
+{
+    return false;
+}
+
+std::vector<std::string>
+CallDevices::names(bool, const std::string &) const
+{
+    return {};
+}
+
+std::vector<std::string>
+CallDevices::resolutions(const std::string &) const
+{
+    return {};
+}
+
+std::vector<std::string>
+CallDevices::frameRates(const std::string &, const std::string &) const
+{
+    return {};
+}
+
+#endif
diff --git a/src/voip/CallDevices.h b/src/voip/CallDevices.h
new file mode 100644
index 0000000000000000000000000000000000000000..d30ce6446a1631a8c1a126cb617d6bfededfc32d
--- /dev/null
+++ b/src/voip/CallDevices.h
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <QObject>
+
+typedef struct _GstDevice GstDevice;
+
+class CallDevices : public QObject
+{
+    Q_OBJECT
+
+public:
+    static CallDevices &instance()
+    {
+        static CallDevices instance;
+        return instance;
+    }
+
+    bool haveMic() const;
+    bool haveCamera() const;
+    std::vector<std::string> names(bool isVideo, const std::string &defaultDevice) const;
+    std::vector<std::string> resolutions(const std::string &cameraName) const;
+    std::vector<std::string> frameRates(const std::string &cameraName,
+                                        const std::string &resolution) const;
+
+signals:
+    void devicesChanged();
+
+private:
+    CallDevices();
+
+    friend class WebRTCSession;
+    void init();
+    GstDevice *audioDevice() const;
+    GstDevice *videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const;
+
+public:
+    CallDevices(CallDevices const &) = delete;
+    void operator=(CallDevices const &) = delete;
+};
diff --git a/src/voip/CallManager.cpp b/src/voip/CallManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0f701b0d2fe212b2694d9bd06908c11eae95ec5c
--- /dev/null
+++ b/src/voip/CallManager.cpp
@@ -0,0 +1,680 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <cctype>
+#include <chrono>
+#include <cstdint>
+#include <cstdlib>
+#include <memory>
+
+#include <QMediaPlaylist>
+#include <QUrl>
+
+#include "Cache.h"
+#include "CallDevices.h"
+#include "CallManager.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "UserSettingsPage.h"
+#include "Utils.h"
+
+#include "mtx/responses/turn_server.hpp"
+
+#ifdef XCB_AVAILABLE
+#include <xcb/xcb.h>
+#include <xcb/xcb_ewmh.h>
+#endif
+
+#ifdef GSTREAMER_AVAILABLE
+extern "C"
+{
+#include "gst/gst.h"
+}
+#endif
+
+Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
+Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
+Q_DECLARE_METATYPE(mtx::responses::TurnServer)
+
+using namespace mtx::events;
+using namespace mtx::events::msg;
+
+using webrtc::CallType;
+
+namespace {
+std::vector<std::string>
+getTurnURIs(const mtx::responses::TurnServer &turnServer);
+}
+
+CallManager::CallManager(QObject *parent)
+  : QObject(parent)
+  , session_(WebRTCSession::instance())
+  , turnServerTimer_(this)
+{
+    qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
+    qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
+    qRegisterMetaType<mtx::responses::TurnServer>();
+
+    connect(
+      &session_,
+      &WebRTCSession::offerCreated,
+      this,
+      [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
+          nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
+          emit newMessage(roomid_, CallInvite{callid_, sdp, "0", timeoutms_});
+          emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
+          std::string callid(callid_);
+          QTimer::singleShot(timeoutms_, this, [this, callid]() {
+              if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) {
+                  hangUp(CallHangUp::Reason::InviteTimeOut);
+                  emit ChatPage::instance()->showNotification("The remote side failed to pick up.");
+              }
+          });
+      });
+
+    connect(
+      &session_,
+      &WebRTCSession::answerCreated,
+      this,
+      [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
+          nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
+          emit newMessage(roomid_, CallAnswer{callid_, sdp, "0"});
+          emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
+      });
+
+    connect(&session_,
+            &WebRTCSession::newICECandidate,
+            this,
+            [this](const CallCandidates::Candidate &candidate) {
+                nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
+                emit newMessage(roomid_, CallCandidates{callid_, {candidate}, "0"});
+            });
+
+    connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
+
+    connect(
+      this, &CallManager::turnServerRetrieved, this, [this](const mtx::responses::TurnServer &res) {
+          nhlog::net()->info("TURN server(s) retrieved from homeserver:");
+          nhlog::net()->info("username: {}", res.username);
+          nhlog::net()->info("ttl: {} seconds", res.ttl);
+          for (const auto &u : res.uris)
+              nhlog::net()->info("uri: {}", u);
+
+          // Request new credentials close to expiry
+          // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
+          turnURIs_    = getTurnURIs(res);
+          uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
+          if (res.ttl < 3600)
+              nhlog::net()->warn("Setting ttl to 1 hour");
+          turnServerTimer_.setInterval(ttl * 1000 * 0.9);
+      });
+
+    connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
+        switch (state) {
+        case webrtc::State::DISCONNECTED:
+            playRingtone(QUrl("qrc:/media/media/callend.ogg"), false);
+            clear();
+            break;
+        case webrtc::State::ICEFAILED: {
+            QString error("Call connection failed.");
+            if (turnURIs_.empty())
+                error += " Your homeserver has no configured TURN server.";
+            emit ChatPage::instance()->showNotification(error);
+            hangUp(CallHangUp::Reason::ICEFailed);
+            break;
+        }
+        default:
+            break;
+        }
+        emit newCallState();
+    });
+
+    connect(
+      &CallDevices::instance(), &CallDevices::devicesChanged, this, &CallManager::devicesChanged);
+
+    connect(
+      &player_, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) {
+          if (status == QMediaPlayer::LoadedMedia)
+              player_.play();
+      });
+
+    connect(&player_,
+            QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error),
+            [this](QMediaPlayer::Error error) {
+                stopRingtone();
+                switch (error) {
+                case QMediaPlayer::FormatError:
+                case QMediaPlayer::ResourceError:
+                    nhlog::ui()->error("WebRTC: valid ringtone file not found");
+                    break;
+                case QMediaPlayer::AccessDeniedError:
+                    nhlog::ui()->error("WebRTC: access to ringtone file denied");
+                    break;
+                default:
+                    nhlog::ui()->error("WebRTC: unable to play ringtone");
+                    break;
+                }
+            });
+}
+
+void
+CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
+{
+    if (isOnCall())
+        return;
+    if (callType == CallType::SCREEN) {
+        if (!screenShareSupported())
+            return;
+        if (windows_.empty() || windowIndex >= windows_.size()) {
+            nhlog::ui()->error("WebRTC: window index out of range");
+            return;
+        }
+    }
+
+    auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
+    if (roomInfo.member_count != 2) {
+        emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms.");
+        return;
+    }
+
+    std::string errorMessage;
+    if (!session_.havePlugins(false, &errorMessage) ||
+        ((callType == CallType::VIDEO || callType == CallType::SCREEN) &&
+         !session_.havePlugins(true, &errorMessage))) {
+        emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
+        return;
+    }
+
+    callType_ = callType;
+    roomid_   = roomid;
+    session_.setTurnServers(turnURIs_);
+    generateCallID();
+    std::string strCallType =
+      callType_ == CallType::VOICE ? "voice" : (callType_ == CallType::VIDEO ? "video" : "screen");
+    nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType);
+    std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
+    const RoomMember &callee =
+      members.front().user_id == utils::localUser() ? members.back() : members.front();
+    callParty_            = callee.user_id;
+    callPartyDisplayName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
+    callPartyAvatarUrl_   = QString::fromStdString(roomInfo.avatar_url);
+    emit newInviteState();
+    playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true);
+    if (!session_.createOffer(callType,
+                              callType == CallType::SCREEN ? windows_[windowIndex].second : 0)) {
+        emit ChatPage::instance()->showNotification("Problem setting up call.");
+        endCall();
+    }
+}
+
+namespace {
+std::string
+callHangUpReasonString(CallHangUp::Reason reason)
+{
+    switch (reason) {
+    case CallHangUp::Reason::ICEFailed:
+        return "ICE failed";
+    case CallHangUp::Reason::InviteTimeOut:
+        return "Invite time out";
+    default:
+        return "User";
+    }
+}
+}
+
+void
+CallManager::hangUp(CallHangUp::Reason reason)
+{
+    if (!callid_.empty()) {
+        nhlog::ui()->debug(
+          "WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
+        emit newMessage(roomid_, CallHangUp{callid_, "0", reason});
+        endCall();
+    }
+}
+
+void
+CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
+{
+#ifdef GSTREAMER_AVAILABLE
+    if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) ||
+        handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event))
+        return;
+#else
+    (void)event;
+#endif
+}
+
+template<typename T>
+bool
+CallManager::handleEvent(const mtx::events::collections::TimelineEvents &event)
+{
+    if (std::holds_alternative<RoomEvent<T>>(event)) {
+        handleEvent(std::get<RoomEvent<T>>(event));
+        return true;
+    }
+    return false;
+}
+
+void
+CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
+{
+    const char video[]     = "m=video";
+    const std::string &sdp = callInviteEvent.content.sdp;
+    bool isVideo           = std::search(sdp.cbegin(),
+                               sdp.cend(),
+                               std::cbegin(video),
+                               std::cend(video) - 1,
+                               [](unsigned char c1, unsigned char c2) {
+                                   return std::tolower(c1) == std::tolower(c2);
+                               }) != sdp.cend();
+
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
+                       callInviteEvent.content.call_id,
+                       (isVideo ? "video" : "voice"),
+                       callInviteEvent.sender);
+
+    if (callInviteEvent.content.call_id.empty())
+        return;
+
+    auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
+    if (isOnCall() || roomInfo.member_count != 2) {
+        emit newMessage(
+          QString::fromStdString(callInviteEvent.room_id),
+          CallHangUp{callInviteEvent.content.call_id, "0", CallHangUp::Reason::InviteTimeOut});
+        return;
+    }
+
+    const QString &ringtone = ChatPage::instance()->userSettings()->ringtone();
+    if (ringtone != "Mute")
+        playRingtone(ringtone == "Default" ? QUrl("qrc:/media/media/ring.ogg")
+                                           : QUrl::fromLocalFile(ringtone),
+                     true);
+    roomid_ = QString::fromStdString(callInviteEvent.room_id);
+    callid_ = callInviteEvent.content.call_id;
+    remoteICECandidates_.clear();
+
+    std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
+    const RoomMember &caller =
+      members.front().user_id == utils::localUser() ? members.back() : members.front();
+    callParty_            = caller.user_id;
+    callPartyDisplayName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
+    callPartyAvatarUrl_   = QString::fromStdString(roomInfo.avatar_url);
+
+    haveCallInvite_ = true;
+    callType_       = isVideo ? CallType::VIDEO : CallType::VOICE;
+    inviteSDP_      = callInviteEvent.content.sdp;
+    emit newInviteState();
+}
+
+void
+CallManager::acceptInvite()
+{
+    if (!haveCallInvite_)
+        return;
+
+    stopRingtone();
+    std::string errorMessage;
+    if (!session_.havePlugins(false, &errorMessage) ||
+        (callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) {
+        emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
+        hangUp();
+        return;
+    }
+
+    session_.setTurnServers(turnURIs_);
+    if (!session_.acceptOffer(inviteSDP_)) {
+        emit ChatPage::instance()->showNotification("Problem setting up call.");
+        hangUp();
+        return;
+    }
+    session_.acceptICECandidates(remoteICECandidates_);
+    remoteICECandidates_.clear();
+    haveCallInvite_ = false;
+    emit newInviteState();
+}
+
+void
+CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
+{
+    if (callCandidatesEvent.sender == utils::localUser().toStdString())
+        return;
+
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
+                       callCandidatesEvent.content.call_id,
+                       callCandidatesEvent.sender);
+
+    if (callid_ == callCandidatesEvent.content.call_id) {
+        if (isOnCall())
+            session_.acceptICECandidates(callCandidatesEvent.content.candidates);
+        else {
+            // CallInvite has been received and we're awaiting localUser to accept or
+            // reject the call
+            for (const auto &c : callCandidatesEvent.content.candidates)
+                remoteICECandidates_.push_back(c);
+        }
+    }
+}
+
+void
+CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
+{
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
+                       callAnswerEvent.content.call_id,
+                       callAnswerEvent.sender);
+
+    if (callAnswerEvent.sender == utils::localUser().toStdString() &&
+        callid_ == callAnswerEvent.content.call_id) {
+        if (!isOnCall()) {
+            emit ChatPage::instance()->showNotification("Call answered on another device.");
+            stopRingtone();
+            haveCallInvite_ = false;
+            emit newInviteState();
+        }
+        return;
+    }
+
+    if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
+        stopRingtone();
+        if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
+            emit ChatPage::instance()->showNotification("Problem setting up call.");
+            hangUp();
+        }
+    }
+}
+
+void
+CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
+{
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
+                       callHangUpEvent.content.call_id,
+                       callHangUpReasonString(callHangUpEvent.content.reason),
+                       callHangUpEvent.sender);
+
+    if (callid_ == callHangUpEvent.content.call_id)
+        endCall();
+}
+
+void
+CallManager::toggleMicMute()
+{
+    session_.toggleMicMute();
+    emit micMuteChanged();
+}
+
+bool
+CallManager::callsSupported()
+{
+#ifdef GSTREAMER_AVAILABLE
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool
+CallManager::screenShareSupported()
+{
+    return std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY");
+}
+
+QStringList
+CallManager::devices(bool isVideo) const
+{
+    QStringList ret;
+    const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera()
+                                           : ChatPage::instance()->userSettings()->microphone();
+    std::vector<std::string> devices =
+      CallDevices::instance().names(isVideo, defaultDevice.toStdString());
+    ret.reserve(devices.size());
+    std::transform(devices.cbegin(), devices.cend(), std::back_inserter(ret), [](const auto &d) {
+        return QString::fromStdString(d);
+    });
+
+    return ret;
+}
+
+void
+CallManager::generateCallID()
+{
+    using namespace std::chrono;
+    uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
+    callid_     = "c" + std::to_string(ms);
+}
+
+void
+CallManager::clear()
+{
+    roomid_.clear();
+    callParty_.clear();
+    callPartyDisplayName_.clear();
+    callPartyAvatarUrl_.clear();
+    callid_.clear();
+    callType_       = CallType::VOICE;
+    haveCallInvite_ = false;
+    emit newInviteState();
+    inviteSDP_.clear();
+    remoteICECandidates_.clear();
+}
+
+void
+CallManager::endCall()
+{
+    stopRingtone();
+    session_.end();
+    clear();
+}
+
+void
+CallManager::refreshTurnServer()
+{
+    turnURIs_.clear();
+    turnServerTimer_.start(2000);
+}
+
+void
+CallManager::retrieveTurnServer()
+{
+    http::client()->get_turn_server(
+      [this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
+          if (err) {
+              turnServerTimer_.setInterval(5000);
+              return;
+          }
+          emit turnServerRetrieved(res);
+      });
+}
+
+void
+CallManager::playRingtone(const QUrl &ringtone, bool repeat)
+{
+    static QMediaPlaylist playlist;
+    playlist.clear();
+    playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
+                                    : QMediaPlaylist::CurrentItemOnce);
+    playlist.addMedia(ringtone);
+    player_.setVolume(100);
+    player_.setPlaylist(&playlist);
+}
+
+void
+CallManager::stopRingtone()
+{
+    player_.setPlaylist(nullptr);
+}
+
+QStringList
+CallManager::windowList()
+{
+    windows_.clear();
+    windows_.push_back({tr("Entire screen"), 0});
+
+#ifdef XCB_AVAILABLE
+    std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection(
+      xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); });
+    if (xcb_connection_has_error(connection.get())) {
+        nhlog::ui()->error("Failed to connect to X server");
+        return {};
+    }
+
+    xcb_ewmh_connection_t ewmh;
+    if (!xcb_ewmh_init_atoms_replies(
+          &ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) {
+        nhlog::ui()->error("Failed to connect to EWMH server");
+        return {};
+    }
+    std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>>
+      ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); });
+
+    for (int i = 0; i < ewmh.nb_screens; i++) {
+        xcb_ewmh_get_windows_reply_t clients;
+        if (!xcb_ewmh_get_client_list_reply(
+              &ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) {
+            nhlog::ui()->error("Failed to request window list");
+            return {};
+        }
+
+        for (uint32_t w = 0; w < clients.windows_len; w++) {
+            xcb_window_t window = clients.windows[w];
+
+            std::string name;
+            xcb_ewmh_get_utf8_strings_reply_t data;
+            auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) {
+                std::string name(r->strings, r->strings_len);
+                xcb_ewmh_get_utf8_strings_reply_wipe(r);
+                return name;
+            };
+
+            xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window);
+            if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr))
+                name = getName(&data);
+
+            cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window);
+            if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr))
+                name = getName(&data);
+
+            windows_.push_back({QString::fromStdString(name), window});
+        }
+        xcb_ewmh_get_windows_reply_wipe(&clients);
+    }
+#endif
+    QStringList ret;
+    ret.reserve(windows_.size());
+    for (const auto &w : windows_)
+        ret.append(w.first);
+
+    return ret;
+}
+
+#ifdef GSTREAMER_AVAILABLE
+namespace {
+
+GstElement *pipe_        = nullptr;
+unsigned int busWatchId_ = 0;
+
+gboolean
+newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer G_GNUC_UNUSED)
+{
+    switch (GST_MESSAGE_TYPE(msg)) {
+    case GST_MESSAGE_EOS:
+        if (pipe_) {
+            gst_element_set_state(GST_ELEMENT(pipe_), GST_STATE_NULL);
+            gst_object_unref(pipe_);
+            pipe_ = nullptr;
+        }
+        if (busWatchId_) {
+            g_source_remove(busWatchId_);
+            busWatchId_ = 0;
+        }
+        break;
+    default:
+        break;
+    }
+    return TRUE;
+}
+}
+#endif
+
+void
+CallManager::previewWindow(unsigned int index) const
+{
+#ifdef GSTREAMER_AVAILABLE
+    if (windows_.empty() || index >= windows_.size() || !gst_is_initialized())
+        return;
+
+    GstElement *ximagesrc = gst_element_factory_make("ximagesrc", nullptr);
+    if (!ximagesrc) {
+        nhlog::ui()->error("Failed to create ximagesrc");
+        return;
+    }
+    GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
+    GstElement *videoscale   = gst_element_factory_make("videoscale", nullptr);
+    GstElement *capsfilter   = gst_element_factory_make("capsfilter", nullptr);
+    GstElement *ximagesink   = gst_element_factory_make("ximagesink", nullptr);
+
+    g_object_set(ximagesrc, "use-damage", FALSE, nullptr);
+    g_object_set(ximagesrc, "show-pointer", FALSE, nullptr);
+    g_object_set(ximagesrc, "xid", windows_[index].second, nullptr);
+
+    GstCaps *caps = gst_caps_new_simple(
+      "video/x-raw", "width", G_TYPE_INT, 480, "height", G_TYPE_INT, 360, nullptr);
+    g_object_set(capsfilter, "caps", caps, nullptr);
+    gst_caps_unref(caps);
+
+    pipe_ = gst_pipeline_new(nullptr);
+    gst_bin_add_many(
+      GST_BIN(pipe_), ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr);
+    if (!gst_element_link_many(
+          ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr)) {
+        nhlog::ui()->error("Failed to link preview window elements");
+        gst_object_unref(pipe_);
+        pipe_ = nullptr;
+        return;
+    }
+    if (gst_element_set_state(pipe_, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
+        nhlog::ui()->error("Unable to start preview pipeline");
+        gst_object_unref(pipe_);
+        pipe_ = nullptr;
+        return;
+    }
+
+    GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
+    busWatchId_ = gst_bus_add_watch(bus, newBusMessage, nullptr);
+    gst_object_unref(bus);
+#else
+    (void)index;
+#endif
+}
+
+namespace {
+std::vector<std::string>
+getTurnURIs(const mtx::responses::TurnServer &turnServer)
+{
+    // gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
+    // where username and password are percent-encoded
+    std::vector<std::string> ret;
+    for (const auto &uri : turnServer.uris) {
+        if (auto c = uri.find(':'); c == std::string::npos) {
+            nhlog::ui()->error("Invalid TURN server uri: {}", uri);
+            continue;
+        } else {
+            std::string scheme = std::string(uri, 0, c);
+            if (scheme != "turn" && scheme != "turns") {
+                nhlog::ui()->error("Invalid TURN server uri: {}", uri);
+                continue;
+            }
+
+            QString encodedUri =
+              QString::fromStdString(scheme) + "://" +
+              QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) + ":" +
+              QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) + "@" +
+              QString::fromStdString(std::string(uri, ++c));
+            ret.push_back(encodedUri.toStdString());
+        }
+    }
+    return ret;
+}
+}
diff --git a/src/voip/CallManager.h b/src/voip/CallManager.h
new file mode 100644
index 0000000000000000000000000000000000000000..22f31814304e5484a4e4bf268772fed43bea2bf5
--- /dev/null
+++ b/src/voip/CallManager.h
@@ -0,0 +1,117 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <QMediaPlayer>
+#include <QObject>
+#include <QString>
+#include <QTimer>
+
+#include "CallDevices.h"
+#include "WebRTCSession.h"
+#include "mtx/events/collections.hpp"
+#include "mtx/events/voip.hpp"
+
+namespace mtx::responses {
+struct TurnServer;
+}
+
+class QStringList;
+class QUrl;
+
+class CallManager : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState)
+    Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
+    Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState)
+    Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState)
+    Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState)
+    Q_PROPERTY(QString callPartyDisplayName READ callPartyDisplayName NOTIFY newInviteState)
+    Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState)
+    Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
+    Q_PROPERTY(bool haveLocalPiP READ haveLocalPiP NOTIFY newCallState)
+    Q_PROPERTY(QStringList mics READ mics NOTIFY devicesChanged)
+    Q_PROPERTY(QStringList cameras READ cameras NOTIFY devicesChanged)
+    Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
+    Q_PROPERTY(bool screenShareSupported READ screenShareSupported CONSTANT)
+
+public:
+    CallManager(QObject *);
+
+    bool haveCallInvite() const { return haveCallInvite_; }
+    bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; }
+    webrtc::CallType callType() const { return callType_; }
+    webrtc::State callState() const { return session_.state(); }
+    QString callParty() const { return callParty_; }
+    QString callPartyDisplayName() const { return callPartyDisplayName_; }
+    QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
+    bool isMicMuted() const { return session_.isMicMuted(); }
+    bool haveLocalPiP() const { return session_.haveLocalPiP(); }
+    QStringList mics() const { return devices(false); }
+    QStringList cameras() const { return devices(true); }
+    void refreshTurnServer();
+
+    static bool callsSupported();
+    static bool screenShareSupported();
+
+public slots:
+    void sendInvite(const QString &roomid, webrtc::CallType, unsigned int windowIndex = 0);
+    void syncEvent(const mtx::events::collections::TimelineEvents &event);
+    void toggleMicMute();
+    void toggleLocalPiP() { session_.toggleLocalPiP(); }
+    void acceptInvite();
+    void hangUp(mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
+    QStringList windowList();
+    void previewWindow(unsigned int windowIndex) const;
+
+signals:
+    void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
+    void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
+    void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
+    void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
+    void newInviteState();
+    void newCallState();
+    void micMuteChanged();
+    void devicesChanged();
+    void turnServerRetrieved(const mtx::responses::TurnServer &);
+
+private slots:
+    void retrieveTurnServer();
+
+private:
+    WebRTCSession &session_;
+    QString roomid_;
+    QString callParty_;
+    QString callPartyDisplayName_;
+    QString callPartyAvatarUrl_;
+    std::string callid_;
+    const uint32_t timeoutms_  = 120000;
+    webrtc::CallType callType_ = webrtc::CallType::VOICE;
+    bool haveCallInvite_       = false;
+    std::string inviteSDP_;
+    std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_;
+    std::vector<std::string> turnURIs_;
+    QTimer turnServerTimer_;
+    QMediaPlayer player_;
+    std::vector<std::pair<QString, uint32_t>> windows_;
+
+    template<typename T>
+    bool handleEvent(const mtx::events::collections::TimelineEvents &event);
+    void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
+    void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
+    void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
+    void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
+    void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo);
+    void generateCallID();
+    QStringList devices(bool isVideo) const;
+    void clear();
+    void endCall();
+    void playRingtone(const QUrl &ringtone, bool repeat);
+    void stopRingtone();
+};
diff --git a/src/voip/WebRTCSession.cpp b/src/voip/WebRTCSession.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..801a365cb313ba4d53359f0667f89a237ccd1e7f
--- /dev/null
+++ b/src/voip/WebRTCSession.cpp
@@ -0,0 +1,1155 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <QQmlEngine>
+#include <QQuickItem>
+#include <algorithm>
+#include <cctype>
+#include <chrono>
+#include <cstdlib>
+#include <cstring>
+#include <optional>
+#include <string_view>
+#include <thread>
+#include <utility>
+
+#include "CallDevices.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "UserSettingsPage.h"
+#include "WebRTCSession.h"
+
+#ifdef GSTREAMER_AVAILABLE
+extern "C"
+{
+#include "gst/gst.h"
+#include "gst/sdp/sdp.h"
+
+#define GST_USE_UNSTABLE_API
+#include "gst/webrtc/webrtc.h"
+}
+#endif
+
+// https://github.com/vector-im/riot-web/issues/10173
+#define STUN_SERVER "stun://turn.matrix.org:3478"
+
+Q_DECLARE_METATYPE(webrtc::CallType)
+Q_DECLARE_METATYPE(webrtc::State)
+
+using webrtc::CallType;
+using webrtc::State;
+
+WebRTCSession::WebRTCSession()
+  : devices_(CallDevices::instance())
+{
+    qRegisterMetaType<webrtc::CallType>();
+    qmlRegisterUncreatableMetaObject(
+      webrtc::staticMetaObject, "im.nheko", 1, 0, "CallType", "Can't instantiate enum");
+
+    qRegisterMetaType<webrtc::State>();
+    qmlRegisterUncreatableMetaObject(
+      webrtc::staticMetaObject, "im.nheko", 1, 0, "WebRTCState", "Can't instantiate enum");
+
+    connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
+    init();
+}
+
+bool
+WebRTCSession::init(std::string *errorMessage)
+{
+#ifdef GSTREAMER_AVAILABLE
+    if (initialised_)
+        return true;
+
+    GError *error = nullptr;
+    if (!gst_init_check(nullptr, nullptr, &error)) {
+        std::string strError("WebRTC: failed to initialise GStreamer: ");
+        if (error) {
+            strError += error->message;
+            g_error_free(error);
+        }
+        nhlog::ui()->error(strError);
+        if (errorMessage)
+            *errorMessage = strError;
+        return false;
+    }
+
+    initialised_   = true;
+    gchar *version = gst_version_string();
+    nhlog::ui()->info("WebRTC: initialised {}", version);
+    g_free(version);
+    devices_.init();
+    return true;
+#else
+    (void)errorMessage;
+    return false;
+#endif
+}
+
+#ifdef GSTREAMER_AVAILABLE
+namespace {
+
+std::string localsdp_;
+std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
+bool haveAudioStream_     = false;
+bool haveVideoStream_     = false;
+GstPad *localPiPSinkPad_  = nullptr;
+GstPad *remotePiPSinkPad_ = nullptr;
+
+gboolean
+newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
+{
+    WebRTCSession *session = static_cast<WebRTCSession *>(user_data);
+    switch (GST_MESSAGE_TYPE(msg)) {
+    case GST_MESSAGE_EOS:
+        nhlog::ui()->error("WebRTC: end of stream");
+        session->end();
+        break;
+    case GST_MESSAGE_ERROR:
+        GError *error;
+        gchar *debug;
+        gst_message_parse_error(msg, &error, &debug);
+        nhlog::ui()->error(
+          "WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message);
+        g_clear_error(&error);
+        g_free(debug);
+        session->end();
+        break;
+    default:
+        break;
+    }
+    return TRUE;
+}
+
+GstWebRTCSessionDescription *
+parseSDP(const std::string &sdp, GstWebRTCSDPType type)
+{
+    GstSDPMessage *msg;
+    gst_sdp_message_new(&msg);
+    if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) {
+        return gst_webrtc_session_description_new(type, msg);
+    } else {
+        nhlog::ui()->error("WebRTC: failed to parse remote session description");
+        gst_sdp_message_free(msg);
+        return nullptr;
+    }
+}
+
+void
+setLocalDescription(GstPromise *promise, gpointer webrtc)
+{
+    const GstStructure *reply = gst_promise_get_reply(promise);
+    gboolean isAnswer         = gst_structure_id_has_field(reply, g_quark_from_string("answer"));
+    GstWebRTCSessionDescription *gstsdp = nullptr;
+    gst_structure_get(
+      reply, isAnswer ? "answer" : "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &gstsdp, nullptr);
+    gst_promise_unref(promise);
+    g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr);
+
+    gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
+    localsdp_  = std::string(sdp);
+    g_free(sdp);
+    gst_webrtc_session_description_free(gstsdp);
+
+    nhlog::ui()->debug(
+      "WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_);
+}
+
+void
+createOffer(GstElement *webrtc)
+{
+    // create-offer first, then set-local-description
+    GstPromise *promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
+    g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise);
+}
+
+void
+createAnswer(GstPromise *promise, gpointer webrtc)
+{
+    // create-answer first, then set-local-description
+    gst_promise_unref(promise);
+    promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
+    g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise);
+}
+
+void
+iceGatheringStateChanged(GstElement *webrtc,
+                         GParamSpec *pspec G_GNUC_UNUSED,
+                         gpointer user_data G_GNUC_UNUSED)
+{
+    GstWebRTCICEGatheringState newState;
+    g_object_get(webrtc, "ice-gathering-state", &newState, nullptr);
+    if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) {
+        nhlog::ui()->debug("WebRTC: GstWebRTCICEGatheringState -> Complete");
+        if (WebRTCSession::instance().isOffering()) {
+            emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_);
+            emit WebRTCSession::instance().stateChanged(State::OFFERSENT);
+        } else {
+            emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_);
+            emit WebRTCSession::instance().stateChanged(State::ANSWERSENT);
+        }
+    }
+}
+
+void
+addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED,
+                     guint mlineIndex,
+                     gchar *candidate,
+                     gpointer G_GNUC_UNUSED)
+{
+    nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
+    localcandidates_.push_back({std::string() /*max-bundle*/, (uint16_t)mlineIndex, candidate});
+}
+
+void
+iceConnectionStateChanged(GstElement *webrtc,
+                          GParamSpec *pspec G_GNUC_UNUSED,
+                          gpointer user_data G_GNUC_UNUSED)
+{
+    GstWebRTCICEConnectionState newState;
+    g_object_get(webrtc, "ice-connection-state", &newState, nullptr);
+    switch (newState) {
+    case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING:
+        nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking");
+        emit WebRTCSession::instance().stateChanged(State::CONNECTING);
+        break;
+    case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED:
+        nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed");
+        emit WebRTCSession::instance().stateChanged(State::ICEFAILED);
+        break;
+    default:
+        break;
+    }
+}
+
+// https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1164
+struct KeyFrameRequestData
+{
+    GstElement *pipe      = nullptr;
+    GstElement *decodebin = nullptr;
+    gint packetsLost      = 0;
+    guint timerid         = 0;
+    std::string statsField;
+} keyFrameRequestData_;
+
+void
+sendKeyFrameRequest()
+{
+    GstPad *sinkpad = gst_element_get_static_pad(keyFrameRequestData_.decodebin, "sink");
+    if (!gst_pad_push_event(sinkpad,
+                            gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM,
+                                                 gst_structure_new_empty("GstForceKeyUnit"))))
+        nhlog::ui()->error("WebRTC: key frame request failed");
+    else
+        nhlog::ui()->debug("WebRTC: sent key frame request");
+
+    gst_object_unref(sinkpad);
+}
+
+void
+testPacketLoss_(GstPromise *promise, gpointer G_GNUC_UNUSED)
+{
+    const GstStructure *reply = gst_promise_get_reply(promise);
+    gint packetsLost          = 0;
+    GstStructure *rtpStats;
+    if (!gst_structure_get(
+          reply, keyFrameRequestData_.statsField.c_str(), GST_TYPE_STRUCTURE, &rtpStats, nullptr)) {
+        nhlog::ui()->error("WebRTC: get-stats: no field: {}", keyFrameRequestData_.statsField);
+        gst_promise_unref(promise);
+        return;
+    }
+    gst_structure_get_int(rtpStats, "packets-lost", &packetsLost);
+    gst_structure_free(rtpStats);
+    gst_promise_unref(promise);
+    if (packetsLost > keyFrameRequestData_.packetsLost) {
+        nhlog::ui()->debug("WebRTC: inbound video lost packet count: {}", packetsLost);
+        keyFrameRequestData_.packetsLost = packetsLost;
+        sendKeyFrameRequest();
+    }
+}
+
+gboolean
+testPacketLoss(gpointer G_GNUC_UNUSED)
+{
+    if (keyFrameRequestData_.pipe) {
+        GstElement *webrtc  = gst_bin_get_by_name(GST_BIN(keyFrameRequestData_.pipe), "webrtcbin");
+        GstPromise *promise = gst_promise_new_with_change_func(testPacketLoss_, nullptr, nullptr);
+        g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise);
+        gst_object_unref(webrtc);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+void
+setWaitForKeyFrame(GstBin *decodebin G_GNUC_UNUSED, GstElement *element, gpointer G_GNUC_UNUSED)
+{
+    if (!std::strcmp(
+          gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))),
+          "rtpvp8depay"))
+        g_object_set(element, "wait-for-keyframe", TRUE, nullptr);
+}
+
+GstElement *
+newAudioSinkChain(GstElement *pipe)
+{
+    GstElement *queue    = gst_element_factory_make("queue", nullptr);
+    GstElement *convert  = gst_element_factory_make("audioconvert", nullptr);
+    GstElement *resample = gst_element_factory_make("audioresample", nullptr);
+    GstElement *sink     = gst_element_factory_make("autoaudiosink", nullptr);
+    gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr);
+    gst_element_link_many(queue, convert, resample, sink, nullptr);
+    gst_element_sync_state_with_parent(queue);
+    gst_element_sync_state_with_parent(convert);
+    gst_element_sync_state_with_parent(resample);
+    gst_element_sync_state_with_parent(sink);
+    return queue;
+}
+
+GstElement *
+newVideoSinkChain(GstElement *pipe)
+{
+    // use compositor for now; acceleration needs investigation
+    GstElement *queue          = gst_element_factory_make("queue", nullptr);
+    GstElement *compositor     = gst_element_factory_make("compositor", "compositor");
+    GstElement *glupload       = gst_element_factory_make("glupload", nullptr);
+    GstElement *glcolorconvert = gst_element_factory_make("glcolorconvert", nullptr);
+    GstElement *qmlglsink      = gst_element_factory_make("qmlglsink", nullptr);
+    GstElement *glsinkbin      = gst_element_factory_make("glsinkbin", nullptr);
+    g_object_set(compositor, "background", 1, nullptr);
+    g_object_set(qmlglsink, "widget", WebRTCSession::instance().getVideoItem(), nullptr);
+    g_object_set(glsinkbin, "sink", qmlglsink, nullptr);
+    gst_bin_add_many(
+      GST_BIN(pipe), queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
+    gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
+    gst_element_sync_state_with_parent(queue);
+    gst_element_sync_state_with_parent(compositor);
+    gst_element_sync_state_with_parent(glupload);
+    gst_element_sync_state_with_parent(glcolorconvert);
+    gst_element_sync_state_with_parent(glsinkbin);
+    return queue;
+}
+
+std::pair<int, int>
+getResolution(GstPad *pad)
+{
+    std::pair<int, int> ret;
+    GstCaps *caps         = gst_pad_get_current_caps(pad);
+    const GstStructure *s = gst_caps_get_structure(caps, 0);
+    gst_structure_get_int(s, "width", &ret.first);
+    gst_structure_get_int(s, "height", &ret.second);
+    gst_caps_unref(caps);
+    return ret;
+}
+
+std::pair<int, int>
+getResolution(GstElement *pipe, const gchar *elementName, const gchar *padName)
+{
+    GstElement *element = gst_bin_get_by_name(GST_BIN(pipe), elementName);
+    GstPad *pad         = gst_element_get_static_pad(element, padName);
+    auto ret            = getResolution(pad);
+    gst_object_unref(pad);
+    gst_object_unref(element);
+    return ret;
+}
+
+std::pair<int, int>
+getPiPDimensions(const std::pair<int, int> &resolution, int fullWidth, double scaleFactor)
+{
+    int pipWidth  = fullWidth * scaleFactor;
+    int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth;
+    return {pipWidth, pipHeight};
+}
+
+void
+addLocalPiP(GstElement *pipe, const std::pair<int, int> &videoCallSize)
+{
+    // embed localUser's camera into received video (CallType::VIDEO)
+    // OR embed screen share into received video (CallType::SCREEN)
+    GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee");
+    if (!tee)
+        return;
+
+    GstElement *queue = gst_element_factory_make("queue", nullptr);
+    gst_bin_add(GST_BIN(pipe), queue);
+    gst_element_link(tee, queue);
+    gst_element_sync_state_with_parent(queue);
+    gst_object_unref(tee);
+
+    GstElement *compositor = gst_bin_get_by_name(GST_BIN(pipe), "compositor");
+    localPiPSinkPad_       = gst_element_get_request_pad(compositor, "sink_%u");
+    g_object_set(localPiPSinkPad_, "zorder", 2, nullptr);
+
+    bool isVideo         = WebRTCSession::instance().callType() == CallType::VIDEO;
+    const gchar *element = isVideo ? "camerafilter" : "screenshare";
+    const gchar *pad     = isVideo ? "sink" : "src";
+    auto resolution      = getResolution(pipe, element, pad);
+    auto pipSize         = getPiPDimensions(resolution, videoCallSize.first, 0.25);
+    nhlog::ui()->debug("WebRTC: local picture-in-picture: {}x{}", pipSize.first, pipSize.second);
+    g_object_set(localPiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr);
+    gint offset = videoCallSize.first / 80;
+    g_object_set(localPiPSinkPad_, "xpos", offset, "ypos", offset, nullptr);
+
+    GstPad *srcpad = gst_element_get_static_pad(queue, "src");
+    if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, localPiPSinkPad_)))
+        nhlog::ui()->error("WebRTC: failed to link local PiP elements");
+    gst_object_unref(srcpad);
+    gst_object_unref(compositor);
+}
+
+void
+addRemotePiP(GstElement *pipe)
+{
+    // embed localUser's camera into screen image being shared
+    if (remotePiPSinkPad_) {
+        auto camRes   = getResolution(pipe, "camerafilter", "sink");
+        auto shareRes = getResolution(pipe, "screenshare", "src");
+        auto pipSize  = getPiPDimensions(camRes, shareRes.first, 0.2);
+        nhlog::ui()->debug(
+          "WebRTC: screen share picture-in-picture: {}x{}", pipSize.first, pipSize.second);
+
+        gint offset = shareRes.first / 100;
+        g_object_set(remotePiPSinkPad_, "zorder", 2, nullptr);
+        g_object_set(remotePiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr);
+        g_object_set(remotePiPSinkPad_,
+                     "xpos",
+                     shareRes.first - pipSize.first - offset,
+                     "ypos",
+                     shareRes.second - pipSize.second - offset,
+                     nullptr);
+    }
+}
+
+void
+addLocalVideo(GstElement *pipe)
+{
+    GstElement *queue = newVideoSinkChain(pipe);
+    GstElement *tee   = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee");
+    GstPad *srcpad    = gst_element_get_request_pad(tee, "src_%u");
+    GstPad *sinkpad   = gst_element_get_static_pad(queue, "sink");
+    if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, sinkpad)))
+        nhlog::ui()->error("WebRTC: failed to link videosrctee -> video sink chain");
+    gst_object_unref(srcpad);
+}
+
+void
+linkNewPad(GstElement *decodebin, GstPad *newpad, GstElement *pipe)
+{
+    GstPad *sinkpad               = gst_element_get_static_pad(decodebin, "sink");
+    GstCaps *sinkcaps             = gst_pad_get_current_caps(sinkpad);
+    const GstStructure *structure = gst_caps_get_structure(sinkcaps, 0);
+
+    gchar *mediaType = nullptr;
+    guint ssrc       = 0;
+    gst_structure_get(
+      structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr);
+    gst_caps_unref(sinkcaps);
+    gst_object_unref(sinkpad);
+
+    WebRTCSession *session = &WebRTCSession::instance();
+    GstElement *queue      = nullptr;
+    if (!std::strcmp(mediaType, "audio")) {
+        nhlog::ui()->debug("WebRTC: received incoming audio stream");
+        haveAudioStream_ = true;
+        queue            = newAudioSinkChain(pipe);
+    } else if (!std::strcmp(mediaType, "video")) {
+        nhlog::ui()->debug("WebRTC: received incoming video stream");
+        if (!session->getVideoItem()) {
+            g_free(mediaType);
+            nhlog::ui()->error("WebRTC: video call item not set");
+            return;
+        }
+        haveVideoStream_ = true;
+        keyFrameRequestData_.statsField =
+          std::string("rtp-inbound-stream-stats_") + std::to_string(ssrc);
+        queue              = newVideoSinkChain(pipe);
+        auto videoCallSize = getResolution(newpad);
+        nhlog::ui()->info(
+          "WebRTC: incoming video resolution: {}x{}", videoCallSize.first, videoCallSize.second);
+        addLocalPiP(pipe, videoCallSize);
+    } else {
+        g_free(mediaType);
+        nhlog::ui()->error("WebRTC: unknown pad type: {}", GST_PAD_NAME(newpad));
+        return;
+    }
+
+    GstPad *queuepad = gst_element_get_static_pad(queue, "sink");
+    if (queuepad) {
+        if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad)))
+            nhlog::ui()->error("WebRTC: unable to link new pad");
+        else {
+            if (session->callType() == CallType::VOICE ||
+                (haveAudioStream_ && (haveVideoStream_ || session->isRemoteVideoRecvOnly()))) {
+                emit session->stateChanged(State::CONNECTED);
+                if (haveVideoStream_) {
+                    keyFrameRequestData_.pipe      = pipe;
+                    keyFrameRequestData_.decodebin = decodebin;
+                    keyFrameRequestData_.timerid =
+                      g_timeout_add_seconds(3, testPacketLoss, nullptr);
+                }
+                addRemotePiP(pipe);
+                if (session->isRemoteVideoRecvOnly())
+                    addLocalVideo(pipe);
+            }
+        }
+        gst_object_unref(queuepad);
+    }
+    g_free(mediaType);
+}
+
+void
+addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
+{
+    if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC)
+        return;
+
+    nhlog::ui()->debug("WebRTC: received incoming stream");
+    GstElement *decodebin = gst_element_factory_make("decodebin", nullptr);
+    // hardware decoding needs investigation; eg rendering fails if vaapi plugin installed
+    g_object_set(decodebin, "force-sw-decoders", TRUE, nullptr);
+    g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe);
+    g_signal_connect(decodebin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr);
+    gst_bin_add(GST_BIN(pipe), decodebin);
+    gst_element_sync_state_with_parent(decodebin);
+    GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink");
+    if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad)))
+        nhlog::ui()->error("WebRTC: unable to link decodebin");
+    gst_object_unref(sinkpad);
+}
+
+bool
+contains(std::string_view str1, std::string_view str2)
+{
+    return std::search(str1.cbegin(),
+                       str1.cend(),
+                       str2.cbegin(),
+                       str2.cend(),
+                       [](unsigned char c1, unsigned char c2) {
+                           return std::tolower(c1) == std::tolower(c2);
+                       }) != str1.cend();
+}
+
+bool
+getMediaAttributes(const GstSDPMessage *sdp,
+                   const char *mediaType,
+                   const char *encoding,
+                   int &payloadType,
+                   bool &recvOnly,
+                   bool &sendOnly)
+{
+    payloadType = -1;
+    recvOnly    = false;
+    sendOnly    = false;
+    for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); ++mlineIndex) {
+        const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex);
+        if (!std::strcmp(gst_sdp_media_get_media(media), mediaType)) {
+            recvOnly            = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr;
+            sendOnly            = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr;
+            const gchar *rtpval = nullptr;
+            for (guint n = 0; n == 0 || rtpval; ++n) {
+                rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n);
+                if (rtpval && contains(rtpval, encoding)) {
+                    payloadType = std::atoi(rtpval);
+                    break;
+                }
+            }
+            return true;
+        }
+    }
+    return false;
+}
+}
+
+bool
+WebRTCSession::havePlugins(bool isVideo, std::string *errorMessage)
+{
+    if (!initialised_ && !init(errorMessage))
+        return false;
+    if (!isVideo && haveVoicePlugins_)
+        return true;
+    if (isVideo && haveVideoPlugins_)
+        return true;
+
+    const gchar *voicePlugins[] = {"audioconvert",
+                                   "audioresample",
+                                   "autodetect",
+                                   "dtls",
+                                   "nice",
+                                   "opus",
+                                   "playback",
+                                   "rtpmanager",
+                                   "srtp",
+                                   "volume",
+                                   "webrtc",
+                                   nullptr};
+
+    const gchar *videoPlugins[] = {
+      "compositor", "opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr};
+
+    std::string strError("Missing GStreamer plugins: ");
+    const gchar **needed  = isVideo ? videoPlugins : voicePlugins;
+    bool &havePlugins     = isVideo ? haveVideoPlugins_ : haveVoicePlugins_;
+    havePlugins           = true;
+    GstRegistry *registry = gst_registry_get();
+    for (guint i = 0; i < g_strv_length((gchar **)needed); i++) {
+        GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]);
+        if (!plugin) {
+            havePlugins = false;
+            strError += std::string(needed[i]) + " ";
+            continue;
+        }
+        gst_object_unref(plugin);
+    }
+    if (!havePlugins) {
+        nhlog::ui()->error(strError);
+        if (errorMessage)
+            *errorMessage = strError;
+        return false;
+    }
+
+    if (isVideo) {
+        // load qmlglsink to register GStreamer's GstGLVideoItem QML type
+        GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr);
+        gst_object_unref(qmlglsink);
+    }
+    return true;
+}
+
+bool
+WebRTCSession::createOffer(CallType callType, uint32_t shareWindowId)
+{
+    clear();
+    isOffering_    = true;
+    callType_      = callType;
+    shareWindowId_ = shareWindowId;
+
+    // opus and vp8 rtp payload types must be defined dynamically
+    // therefore from the range [96-127]
+    // see for example https://tools.ietf.org/html/rfc7587
+    constexpr int opusPayloadType = 111;
+    constexpr int vp8PayloadType  = 96;
+    return startPipeline(opusPayloadType, vp8PayloadType);
+}
+
+bool
+WebRTCSession::acceptOffer(const std::string &sdp)
+{
+    nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp);
+    if (state_ != State::DISCONNECTED)
+        return false;
+
+    clear();
+    GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
+    if (!offer)
+        return false;
+
+    int opusPayloadType;
+    bool recvOnly;
+    bool sendOnly;
+    if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, recvOnly, sendOnly)) {
+        if (opusPayloadType == -1) {
+            nhlog::ui()->error("WebRTC: remote audio offer - no opus encoding");
+            gst_webrtc_session_description_free(offer);
+            return false;
+        }
+    } else {
+        nhlog::ui()->error("WebRTC: remote offer - no audio media");
+        gst_webrtc_session_description_free(offer);
+        return false;
+    }
+
+    int vp8PayloadType;
+    bool isVideo = getMediaAttributes(
+      offer->sdp, "video", "vp8", vp8PayloadType, isRemoteVideoRecvOnly_, isRemoteVideoSendOnly_);
+    if (isVideo && vp8PayloadType == -1) {
+        nhlog::ui()->error("WebRTC: remote video offer - no vp8 encoding");
+        gst_webrtc_session_description_free(offer);
+        return false;
+    }
+    callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
+
+    if (!startPipeline(opusPayloadType, vp8PayloadType)) {
+        gst_webrtc_session_description_free(offer);
+        return false;
+    }
+
+    // avoid a race that sometimes leaves the generated answer without media tracks (a=ssrc
+    // lines)
+    std::this_thread::sleep_for(std::chrono::milliseconds(200));
+
+    // set-remote-description first, then create-answer
+    GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr);
+    g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise);
+    gst_webrtc_session_description_free(offer);
+    return true;
+}
+
+bool
+WebRTCSession::acceptAnswer(const std::string &sdp)
+{
+    nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp);
+    if (state_ != State::OFFERSENT)
+        return false;
+
+    GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
+    if (!answer) {
+        end();
+        return false;
+    }
+
+    if (callType_ != CallType::VOICE) {
+        int unused;
+        if (!getMediaAttributes(
+              answer->sdp, "video", "vp8", unused, isRemoteVideoRecvOnly_, isRemoteVideoSendOnly_))
+            isRemoteVideoRecvOnly_ = true;
+    }
+
+    g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr);
+    gst_webrtc_session_description_free(answer);
+    return true;
+}
+
+void
+WebRTCSession::acceptICECandidates(
+  const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates)
+{
+    if (state_ >= State::INITIATED) {
+        for (const auto &c : candidates) {
+            nhlog::ui()->debug(
+              "WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate);
+            if (!c.candidate.empty()) {
+                g_signal_emit_by_name(
+                  webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
+            }
+        }
+    }
+}
+
+bool
+WebRTCSession::startPipeline(int opusPayloadType, int vp8PayloadType)
+{
+    if (state_ != State::DISCONNECTED)
+        return false;
+
+    emit stateChanged(State::INITIATING);
+
+    if (!createPipeline(opusPayloadType, vp8PayloadType)) {
+        end();
+        return false;
+    }
+
+    webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin");
+
+    if (ChatPage::instance()->userSettings()->useStunServer()) {
+        nhlog::ui()->info("WebRTC: setting STUN server: {}", STUN_SERVER);
+        g_object_set(webrtc_, "stun-server", STUN_SERVER, nullptr);
+    }
+
+    for (const auto &uri : turnServers_) {
+        nhlog::ui()->info("WebRTC: setting TURN server: {}", uri);
+        gboolean udata;
+        g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata));
+    }
+    if (turnServers_.empty())
+        nhlog::ui()->warn("WebRTC: no TURN server provided");
+
+    // generate the offer when the pipeline goes to PLAYING
+    if (isOffering_)
+        g_signal_connect(webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr);
+
+    // on-ice-candidate is emitted when a local ICE candidate has been gathered
+    g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr);
+
+    // capture ICE failure
+    g_signal_connect(
+      webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr);
+
+    // incoming streams trigger pad-added
+    gst_element_set_state(pipe_, GST_STATE_READY);
+    g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_);
+
+    // capture ICE gathering completion
+    g_signal_connect(
+      webrtc_, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), nullptr);
+
+    // webrtcbin lifetime is the same as that of the pipeline
+    gst_object_unref(webrtc_);
+
+    // start the pipeline
+    GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING);
+    if (ret == GST_STATE_CHANGE_FAILURE) {
+        nhlog::ui()->error("WebRTC: unable to start pipeline");
+        end();
+        return false;
+    }
+
+    GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
+    busWatchId_ = gst_bus_add_watch(bus, newBusMessage, this);
+    gst_object_unref(bus);
+    emit stateChanged(State::INITIATED);
+    return true;
+}
+
+bool
+WebRTCSession::createPipeline(int opusPayloadType, int vp8PayloadType)
+{
+    GstDevice *device = devices_.audioDevice();
+    if (!device)
+        return false;
+
+    GstElement *source     = gst_device_create_element(device, nullptr);
+    GstElement *volume     = gst_element_factory_make("volume", "srclevel");
+    GstElement *convert    = gst_element_factory_make("audioconvert", nullptr);
+    GstElement *resample   = gst_element_factory_make("audioresample", nullptr);
+    GstElement *queue1     = gst_element_factory_make("queue", nullptr);
+    GstElement *opusenc    = gst_element_factory_make("opusenc", nullptr);
+    GstElement *rtp        = gst_element_factory_make("rtpopuspay", nullptr);
+    GstElement *queue2     = gst_element_factory_make("queue", nullptr);
+    GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr);
+
+    GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp",
+                                           "media",
+                                           G_TYPE_STRING,
+                                           "audio",
+                                           "encoding-name",
+                                           G_TYPE_STRING,
+                                           "OPUS",
+                                           "payload",
+                                           G_TYPE_INT,
+                                           opusPayloadType,
+                                           nullptr);
+    g_object_set(capsfilter, "caps", rtpcaps, nullptr);
+    gst_caps_unref(rtpcaps);
+
+    GstElement *webrtcbin = gst_element_factory_make("webrtcbin", "webrtcbin");
+    g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr);
+
+    pipe_ = gst_pipeline_new(nullptr);
+    gst_bin_add_many(GST_BIN(pipe_),
+                     source,
+                     volume,
+                     convert,
+                     resample,
+                     queue1,
+                     opusenc,
+                     rtp,
+                     queue2,
+                     capsfilter,
+                     webrtcbin,
+                     nullptr);
+
+    if (!gst_element_link_many(source,
+                               volume,
+                               convert,
+                               resample,
+                               queue1,
+                               opusenc,
+                               rtp,
+                               queue2,
+                               capsfilter,
+                               webrtcbin,
+                               nullptr)) {
+        nhlog::ui()->error("WebRTC: failed to link audio pipeline elements");
+        return false;
+    }
+
+    return callType_ == CallType::VOICE || isRemoteVideoSendOnly_
+             ? true
+             : addVideoPipeline(vp8PayloadType);
+}
+
+bool
+WebRTCSession::addVideoPipeline(int vp8PayloadType)
+{
+    // allow incoming video calls despite localUser having no webcam
+    if (callType_ == CallType::VIDEO && !devices_.haveCamera())
+        return !isOffering_;
+
+    auto settings            = ChatPage::instance()->userSettings();
+    GstElement *camerafilter = nullptr;
+    GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
+    GstElement *tee          = gst_element_factory_make("tee", "videosrctee");
+    gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr);
+    if (callType_ == CallType::VIDEO || (settings->screenSharePiP() && devices_.haveCamera())) {
+        std::pair<int, int> resolution;
+        std::pair<int, int> frameRate;
+        GstDevice *device = devices_.videoDevice(resolution, frameRate);
+        if (!device)
+            return false;
+
+        GstElement *camera = gst_device_create_element(device, nullptr);
+        GstCaps *caps      = gst_caps_new_simple("video/x-raw",
+                                            "width",
+                                            G_TYPE_INT,
+                                            resolution.first,
+                                            "height",
+                                            G_TYPE_INT,
+                                            resolution.second,
+                                            "framerate",
+                                            GST_TYPE_FRACTION,
+                                            frameRate.first,
+                                            frameRate.second,
+                                            nullptr);
+        camerafilter       = gst_element_factory_make("capsfilter", "camerafilter");
+        g_object_set(camerafilter, "caps", caps, nullptr);
+        gst_caps_unref(caps);
+
+        gst_bin_add_many(GST_BIN(pipe_), camera, camerafilter, nullptr);
+        if (!gst_element_link_many(camera, videoconvert, camerafilter, nullptr)) {
+            nhlog::ui()->error("WebRTC: failed to link camera elements");
+            return false;
+        }
+        if (callType_ == CallType::VIDEO && !gst_element_link(camerafilter, tee)) {
+            nhlog::ui()->error("WebRTC: failed to link camerafilter -> tee");
+            return false;
+        }
+    }
+
+    if (callType_ == CallType::SCREEN) {
+        nhlog::ui()->debug("WebRTC: screen share frame rate: {} fps",
+                           settings->screenShareFrameRate());
+        nhlog::ui()->debug("WebRTC: screen share picture-in-picture: {}",
+                           settings->screenSharePiP());
+        nhlog::ui()->debug("WebRTC: screen share request remote camera: {}",
+                           settings->screenShareRemoteVideo());
+        nhlog::ui()->debug("WebRTC: screen share hide mouse cursor: {}",
+                           settings->screenShareHideCursor());
+
+        GstElement *ximagesrc = gst_element_factory_make("ximagesrc", "screenshare");
+        if (!ximagesrc) {
+            nhlog::ui()->error("WebRTC: failed to create ximagesrc");
+            return false;
+        }
+        g_object_set(ximagesrc, "use-damage", FALSE, nullptr);
+        g_object_set(ximagesrc, "xid", shareWindowId_, nullptr);
+        g_object_set(ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr);
+
+        GstCaps *caps          = gst_caps_new_simple("video/x-raw",
+                                            "framerate",
+                                            GST_TYPE_FRACTION,
+                                            settings->screenShareFrameRate(),
+                                            1,
+                                            nullptr);
+        GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr);
+        g_object_set(capsfilter, "caps", caps, nullptr);
+        gst_caps_unref(caps);
+        gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr);
+
+        if (settings->screenSharePiP() && devices_.haveCamera()) {
+            GstElement *compositor = gst_element_factory_make("compositor", nullptr);
+            g_object_set(compositor, "background", 1, nullptr);
+            gst_bin_add(GST_BIN(pipe_), compositor);
+            if (!gst_element_link_many(ximagesrc, compositor, capsfilter, tee, nullptr)) {
+                nhlog::ui()->error("WebRTC: failed to link screen share elements");
+                return false;
+            }
+
+            GstPad *srcpad    = gst_element_get_static_pad(camerafilter, "src");
+            remotePiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u");
+            if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, remotePiPSinkPad_))) {
+                nhlog::ui()->error("WebRTC: failed to link camerafilter -> compositor");
+                gst_object_unref(srcpad);
+                return false;
+            }
+            gst_object_unref(srcpad);
+        } else if (!gst_element_link_many(ximagesrc, videoconvert, capsfilter, tee, nullptr)) {
+            nhlog::ui()->error("WebRTC: failed to link screen share elements");
+            return false;
+        }
+    }
+
+    GstElement *queue  = gst_element_factory_make("queue", nullptr);
+    GstElement *vp8enc = gst_element_factory_make("vp8enc", nullptr);
+    g_object_set(vp8enc, "deadline", 1, nullptr);
+    g_object_set(vp8enc, "error-resilient", 1, nullptr);
+    GstElement *rtpvp8pay     = gst_element_factory_make("rtpvp8pay", nullptr);
+    GstElement *rtpqueue      = gst_element_factory_make("queue", nullptr);
+    GstElement *rtpcapsfilter = gst_element_factory_make("capsfilter", nullptr);
+    GstCaps *rtpcaps          = gst_caps_new_simple("application/x-rtp",
+                                           "media",
+                                           G_TYPE_STRING,
+                                           "video",
+                                           "encoding-name",
+                                           G_TYPE_STRING,
+                                           "VP8",
+                                           "payload",
+                                           G_TYPE_INT,
+                                           vp8PayloadType,
+                                           nullptr);
+    g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr);
+    gst_caps_unref(rtpcaps);
+
+    gst_bin_add_many(GST_BIN(pipe_), queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, nullptr);
+
+    GstElement *webrtcbin = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin");
+    if (!gst_element_link_many(
+          tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) {
+        nhlog::ui()->error("WebRTC: failed to link rtp video elements");
+        gst_object_unref(webrtcbin);
+        return false;
+    }
+
+    if (callType_ == CallType::SCREEN &&
+        !ChatPage::instance()->userSettings()->screenShareRemoteVideo()) {
+        GArray *transceivers;
+        g_signal_emit_by_name(webrtcbin, "get-transceivers", &transceivers);
+        GstWebRTCRTPTransceiver *transceiver =
+          g_array_index(transceivers, GstWebRTCRTPTransceiver *, 1);
+        transceiver->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY;
+        g_array_unref(transceivers);
+    }
+
+    gst_object_unref(webrtcbin);
+    return true;
+}
+
+bool
+WebRTCSession::haveLocalPiP() const
+{
+    if (state_ >= State::INITIATED) {
+        if (callType_ == CallType::VOICE || isRemoteVideoRecvOnly_)
+            return false;
+        else if (callType_ == CallType::SCREEN)
+            return true;
+        else {
+            GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe_), "videosrctee");
+            if (tee) {
+                gst_object_unref(tee);
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+bool
+WebRTCSession::isMicMuted() const
+{
+    if (state_ < State::INITIATED)
+        return false;
+
+    GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
+    gboolean muted;
+    g_object_get(srclevel, "mute", &muted, nullptr);
+    gst_object_unref(srclevel);
+    return muted;
+}
+
+bool
+WebRTCSession::toggleMicMute()
+{
+    if (state_ < State::INITIATED)
+        return false;
+
+    GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
+    gboolean muted;
+    g_object_get(srclevel, "mute", &muted, nullptr);
+    g_object_set(srclevel, "mute", !muted, nullptr);
+    gst_object_unref(srclevel);
+    return !muted;
+}
+
+void
+WebRTCSession::toggleLocalPiP()
+{
+    if (localPiPSinkPad_) {
+        guint zorder;
+        g_object_get(localPiPSinkPad_, "zorder", &zorder, nullptr);
+        g_object_set(localPiPSinkPad_, "zorder", zorder ? 0 : 2, nullptr);
+    }
+}
+
+void
+WebRTCSession::clear()
+{
+    callType_              = webrtc::CallType::VOICE;
+    isOffering_            = false;
+    isRemoteVideoRecvOnly_ = false;
+    isRemoteVideoSendOnly_ = false;
+    videoItem_             = nullptr;
+    pipe_                  = nullptr;
+    webrtc_                = nullptr;
+    busWatchId_            = 0;
+    shareWindowId_         = 0;
+    haveAudioStream_       = false;
+    haveVideoStream_       = false;
+    localPiPSinkPad_       = nullptr;
+    remotePiPSinkPad_      = nullptr;
+    localsdp_.clear();
+    localcandidates_.clear();
+}
+
+void
+WebRTCSession::end()
+{
+    nhlog::ui()->debug("WebRTC: ending session");
+    keyFrameRequestData_ = KeyFrameRequestData{};
+    if (pipe_) {
+        gst_element_set_state(pipe_, GST_STATE_NULL);
+        gst_object_unref(pipe_);
+        pipe_ = nullptr;
+        if (busWatchId_) {
+            g_source_remove(busWatchId_);
+            busWatchId_ = 0;
+        }
+    }
+
+    clear();
+    if (state_ != State::DISCONNECTED)
+        emit stateChanged(State::DISCONNECTED);
+}
+
+#else
+
+bool
+WebRTCSession::havePlugins(bool, std::string *)
+{
+    return false;
+}
+
+bool
+WebRTCSession::haveLocalPiP() const
+{
+    return false;
+}
+
+bool WebRTCSession::createOffer(webrtc::CallType, uint32_t) { return false; }
+
+bool
+WebRTCSession::acceptOffer(const std::string &)
+{
+    return false;
+}
+
+bool
+WebRTCSession::acceptAnswer(const std::string &)
+{
+    return false;
+}
+
+void
+WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &)
+{}
+
+bool
+WebRTCSession::isMicMuted() const
+{
+    return false;
+}
+
+bool
+WebRTCSession::toggleMicMute()
+{
+    return false;
+}
+
+void
+WebRTCSession::toggleLocalPiP()
+{}
+
+void
+WebRTCSession::end()
+{}
+
+#endif
diff --git a/src/voip/WebRTCSession.h b/src/voip/WebRTCSession.h
new file mode 100644
index 0000000000000000000000000000000000000000..56c0a2951f421e20e1f7befcb35ae162715ecfdb
--- /dev/null
+++ b/src/voip/WebRTCSession.h
@@ -0,0 +1,117 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <QObject>
+
+#include "mtx/events/voip.hpp"
+
+typedef struct _GstElement GstElement;
+class CallDevices;
+class QQuickItem;
+
+namespace webrtc {
+Q_NAMESPACE
+
+enum class CallType
+{
+    VOICE,
+    VIDEO,
+    SCREEN // localUser is sharing screen
+};
+Q_ENUM_NS(CallType)
+
+enum class State
+{
+    DISCONNECTED,
+    ICEFAILED,
+    INITIATING,
+    INITIATED,
+    OFFERSENT,
+    ANSWERSENT,
+    CONNECTING,
+    CONNECTED
+
+};
+Q_ENUM_NS(State)
+}
+
+class WebRTCSession : public QObject
+{
+    Q_OBJECT
+
+public:
+    static WebRTCSession &instance()
+    {
+        static WebRTCSession instance;
+        return instance;
+    }
+
+    bool havePlugins(bool isVideo, std::string *errorMessage = nullptr);
+    webrtc::CallType callType() const { return callType_; }
+    webrtc::State state() const { return state_; }
+    bool haveLocalPiP() const;
+    bool isOffering() const { return isOffering_; }
+    bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; }
+    bool isRemoteVideoSendOnly() const { return isRemoteVideoSendOnly_; }
+
+    bool createOffer(webrtc::CallType, uint32_t shareWindowId);
+    bool acceptOffer(const std::string &sdp);
+    bool acceptAnswer(const std::string &sdp);
+    void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
+
+    bool isMicMuted() const;
+    bool toggleMicMute();
+    void toggleLocalPiP();
+    void end();
+
+    void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; }
+
+    void setVideoItem(QQuickItem *item) { videoItem_ = item; }
+    QQuickItem *getVideoItem() const { return videoItem_; }
+
+signals:
+    void offerCreated(const std::string &sdp,
+                      const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
+    void answerCreated(const std::string &sdp,
+                       const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
+    void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &);
+    void stateChanged(webrtc::State);
+
+private slots:
+    void setState(webrtc::State state) { state_ = state; }
+
+private:
+    WebRTCSession();
+
+    CallDevices &devices_;
+    bool initialised_           = false;
+    bool haveVoicePlugins_      = false;
+    bool haveVideoPlugins_      = false;
+    webrtc::CallType callType_  = webrtc::CallType::VOICE;
+    webrtc::State state_        = webrtc::State::DISCONNECTED;
+    bool isOffering_            = false;
+    bool isRemoteVideoRecvOnly_ = false;
+    bool isRemoteVideoSendOnly_ = false;
+    QQuickItem *videoItem_      = nullptr;
+    GstElement *pipe_           = nullptr;
+    GstElement *webrtc_         = nullptr;
+    unsigned int busWatchId_    = 0;
+    std::vector<std::string> turnServers_;
+    uint32_t shareWindowId_ = 0;
+
+    bool init(std::string *errorMessage = nullptr);
+    bool startPipeline(int opusPayloadType, int vp8PayloadType);
+    bool createPipeline(int opusPayloadType, int vp8PayloadType);
+    bool addVideoPipeline(int vp8PayloadType);
+    void clear();
+
+public:
+    WebRTCSession(WebRTCSession const &) = delete;
+    void operator=(WebRTCSession const &) = delete;
+};
diff --git a/third_party/blurhash/blurhash.cpp b/third_party/blurhash/blurhash.cpp
index a4adf89f7acf616e7bf6cc6c08d8bfff2de4e48c..bcfcce5c08bf966cc061b1ef84dd3ed9b1a68ac1 100644
--- a/third_party/blurhash/blurhash.cpp
+++ b/third_party/blurhash/blurhash.cpp
@@ -6,10 +6,6 @@
 #include <cmath>
 #include <stdexcept>
 
-#ifndef M_PI
-#define M_PI 3.14159265358979323846
-#endif
-
 #ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
 #include <doctest.h>
 #endif
@@ -17,6 +13,9 @@
 using namespace std::literals;
 
 namespace {
+template<class T>
+T pi = 3.14159265358979323846;
+
 constexpr std::array<char, 84> int_to_b83{
   "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"};
 
@@ -62,13 +61,13 @@ struct Components
 };
 
 int
-packComponents(const Components &c)
+packComponents(const Components &c) noexcept
 {
         return (c.x - 1) + (c.y - 1) * 9;
 }
 
 Components
-unpackComponents(int c)
+unpackComponents(int c) noexcept
 {
         return {c % 9 + 1, c / 9 + 1};
 }
@@ -88,7 +87,7 @@ decode83(std::string_view value)
 }
 
 float
-decodeMaxAC(int quantizedMaxAC)
+decodeMaxAC(int quantizedMaxAC) noexcept
 {
         return (quantizedMaxAC + 1) / 166.;
 }
@@ -101,13 +100,13 @@ decodeMaxAC(std::string_view maxAC)
 }
 
 int
-encodeMaxAC(float maxAC)
+encodeMaxAC(float maxAC) noexcept
 {
-        return std::max(0, std::min(82, int(maxAC * 166 - 0.5)));
+        return std::max(0, std::min(82, int(maxAC * 166 - 0.5f)));
 }
 
 float
-srgbToLinear(int value)
+srgbToLinear(int value) noexcept
 {
         auto srgbToLinearF = [](float x) {
                 if (x <= 0.0f)
@@ -124,7 +123,7 @@ srgbToLinear(int value)
 }
 
 int
-linearToSrgb(float value)
+linearToSrgb(float value) noexcept
 {
         auto linearToSrgbF = [](float x) -> float {
                 if (x <= 0.0f)
@@ -137,7 +136,7 @@ linearToSrgb(float value)
                         return std::pow(x, 1.0f / 2.4f) * 1.055f - 0.055f;
         };
 
-        return int(linearToSrgbF(value) * 255.f + 0.5);
+        return int(linearToSrgbF(value) * 255.f + 0.5f);
 }
 
 struct Color
@@ -235,8 +234,8 @@ multiplyBasisFunction(Components components, int width, int height, unsigned cha
 
         for (int y = 0; y < height; y++) {
                 for (int x = 0; x < width; x++) {
-                        float basis = std::cos(M_PI * components.x * x / float(width)) *
-                                      std::cos(M_PI * components.y * y / float(height));
+                        float basis = std::cos(pi<float> * components.x * x / float(width)) *
+                                      std::cos(pi<float> * components.y * y / float(height));
                         c.r += basis * srgbToLinear(pixels[3 * x + 0 + y * width * 3]);
                         c.g += basis * srgbToLinear(pixels[3 * x + 1 + y * width * 3]);
                         c.b += basis * srgbToLinear(pixels[3 * x + 2 + y * width * 3]);
@@ -251,7 +250,7 @@ multiplyBasisFunction(Components components, int width, int height, unsigned cha
 
 namespace blurhash {
 Image
-decode(std::string_view blurhash, size_t width, size_t height, size_t bytesPerPixel)
+decode(std::string_view blurhash, size_t width, size_t height, size_t bytesPerPixel) noexcept
 {
         Image i{};
 
@@ -287,8 +286,8 @@ decode(std::string_view blurhash, size_t width, size_t height, size_t bytesPerPi
                         for (size_t nx = 0; nx < size_t(components.x); nx++) {
                                 for (size_t ny = 0; ny < size_t(components.y); ny++) {
                                         float basis =
-                                          std::cos(M_PI * float(x) * float(nx) / float(width)) *
-                                          std::cos(M_PI * float(y) * float(ny) / float(height));
+                                          std::cos(pi<float> * float(nx * x) / float(width)) *
+                                          std::cos(pi<float> * float(ny * y) / float(height));
                                         c += values[nx + ny * components.x] * basis;
                                 }
                         }
diff --git a/third_party/blurhash/blurhash.hpp b/third_party/blurhash/blurhash.hpp
index e01b9b3f9b038b4218af19c15a3f1827ea85619b..d4e138ef8df43ec5aebd2d4ca3d2fe9baf18b874 100644
--- a/third_party/blurhash/blurhash.hpp
+++ b/third_party/blurhash/blurhash.hpp
@@ -13,7 +13,7 @@ struct Image
 
 // Decode a blurhash to an image with size width*height
 Image
-decode(std::string_view blurhash, size_t width, size_t height, size_t bytesPerPixel = 3);
+decode(std::string_view blurhash, size_t width, size_t height, size_t bytesPerPixel = 3) noexcept;
 
 // Encode an image of rgb pixels (without padding) with size width*height into a blurhash with x*y
 // components