summary refs log tree commit diff
diff options
context:
space:
mode:
authorJoseph Donofry <joedonofry@gmail.com>2021-08-16 17:16:17 -0400
committerJoseph Donofry <joedonofry@gmail.com>2021-08-16 17:16:17 -0400
commit093f9f9e338f7898a2e3e3ed5673952639015cab (patch)
tree4117da7ddaf1bb2c2e3dbd8b51277343142f799c
parentMerge origin/master and fix conflicts (diff)
parentUpdate qt5 path in macos deploy.sh script (diff)
downloadnheko-093f9f9e338f7898a2e3e3ed5673952639015cab.tar.xz
Merge remote-tracking branch 'nheko-im/master' into video_player_enhancements
-rwxr-xr-x.ci/macos/deploy.sh2
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md61
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yaml150
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md20
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.yaml49
-rw-r--r--CMakeLists.txt4
-rw-r--r--README.md2
-rw-r--r--io.github.NhekoReborn.Nheko.yaml4
-rw-r--r--resources/qml/Avatar.qml20
-rw-r--r--resources/qml/ForwardCompleter.qml4
-rw-r--r--resources/qml/MessageView.qml1
-rw-r--r--resources/qml/QuickSwitcher.qml3
-rw-r--r--resources/qml/ReplyPopup.qml1
-rw-r--r--resources/qml/RoomList.qml12
-rw-r--r--resources/qml/RoomMembers.qml42
-rw-r--r--resources/qml/RoomSettings.qml4
-rw-r--r--resources/qml/Root.qml5
-rw-r--r--resources/qml/TimelineView.qml10
-rw-r--r--resources/qml/TopBar.qml28
-rw-r--r--resources/qml/dialogs/ImagePackEditorDialog.qml2
-rw-r--r--src/Cache.cpp388
-rw-r--r--src/CacheCryptoStructs.h10
-rw-r--r--src/Cache_p.h45
-rw-r--r--src/ChatPage.cpp2
-rw-r--r--src/ChatPage.h2
-rw-r--r--src/MemberList.cpp12
-rw-r--r--src/MemberList.h1
-rw-r--r--src/MxcImageProvider.cpp96
-rw-r--r--src/MxcImageProvider.h7
-rw-r--r--src/Olm.cpp33
-rw-r--r--src/RegisterPage.cpp18
-rw-r--r--src/UserSettingsPage.cpp3
-rw-r--r--src/UserSettingsPage.h5
-rw-r--r--src/dialogs/ImageOverlay.cpp4
-rw-r--r--src/dialogs/ImageOverlay.h2
-rw-r--r--src/timeline/TimelineModel.cpp19
-rw-r--r--src/timeline/TimelineModel.h3
-rw-r--r--src/timeline/TimelineViewManager.cpp8
-rw-r--r--src/timeline/TimelineViewManager.h4
39 files changed, 793 insertions, 293 deletions
diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh
index 7439a06d..56a1f23a 100755
--- a/.ci/macos/deploy.sh
+++ b/.ci/macos/deploy.sh
@@ -6,7 +6,7 @@ set -eux
 #TAG=$(git tag -l --points-at HEAD)
 
 # Add Qt binaries to path
-PATH=/usr/local/opt/qt/bin/:${PATH}
+PATH=/usr/local/opt/qt@5/bin/:${PATH}
 
 ( cd build
   # macdeployqt does not copy symlinks over.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 5419532b..00000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ''
-labels: bug
-assignees: ''
-
----
-
-### Describe the bug
-A clear and concise description of what the bug is.
-
-### To Reproduce
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-### Expected behavior
-A clear and concise description of what you expected to happen.
-
-### Screenshots
-If applicable, add screenshots to help explain your problem.
-
-### System:
-
-- Nheko version: <!-- Get the version from the settings menu (bottom left corner)  -->
-- Profile used: <!-- If you are not using the default profile, mention it here -->
-- Installation method: <!-- AppImage, some repository, local build etc -->
-- Operating System:
-- Qt version: <!-- If you compiled it yourself -->
-- C++ compiler: <!-- if you compiled it yourself -->
-- Desktop Environment: <!-- for Linux -->
-
-### Logs
-<!-- If applicable -->
-
-<!-- The log file is located in
-    Linux: ~/.cache/nheko/
-    macOS: ~/Library/Caches/nheko or /Library/Caches/nheko
-    Windows: C:/Users/<USER>/AppData/Local/nheko/cache
--->
-
-### Debugger backtrace
-<!-- 
-If the program crashed send a backtrace:
-
-You can retrieve a backtrace by building nheko with -DCMAKE_BUILD_TYPE=Debug
-and running it through gdb or lldb.
-
-gdb ./build/nheko
-
->> run
-
-... Make the program crash
-
->> bt
-
-... Paste a link of the output below (Use a pastebin, don't paste directly in the github issue).
--->
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 00000000..871189e7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,150 @@
+name: Bug Report
+description: Create a report to help us improve
+#title: "[Bug]: "
+labels: [bug]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this bug report!
+        Please try to fill out all fields to the best of your ability.
+  - type: textarea
+    id: description
+    attributes:
+      label: Describe the bug
+      description: A clear and concise description of what the bug is.
+      placeholder: Enter your description here.
+    validations:
+      required: true
+  - type: textarea
+    id: reproduction-steps
+    attributes:
+      label: To Reproduce
+      description: Steps to reproduce the behavior.
+      placeholder: |
+        1. Go to '...'
+        2. Click on '....'
+        3. Scroll down to '....'
+        4. See error
+      value: |
+        1. Go to '...'
+        2. Click on '....'
+        3. Scroll down to '....'
+        4. See error
+    validations:
+      required: true
+  - type: textarea
+    id: behaviour
+    attributes:
+      label: What happened?
+      description: A clear and concise description of what actually happened.
+    validations:
+      required: false
+  - type: textarea
+    id: expected-behaviour
+    attributes:
+      label: Expected behavior
+      description: A clear and concise description of what you expected to happen.
+    validations:
+      required: false
+  - type: textarea
+    id: screenshots
+    attributes:
+      label: Screenshots
+      description: If applicable, add screenshots to help explain your problem.
+      placeholder: Upload your screenshots here. You can paste them or click on "Attach files".
+    validations:
+      required: false
+  - type: input
+    id: version
+    attributes:
+      label: Version
+      description: Get the version from the settings menu (bottom left corner)
+      placeholder: 0.0.1-deafbeef
+    validations:
+      required: true
+  - type: dropdown
+    id: os
+    attributes:
+      label: Operating system
+      multiple: true
+      options:
+        - Linux
+        - macOS
+        - Windows
+        - BSD
+        - Haiku
+        - Other
+  - type: dropdown
+    id: install-method
+    attributes:
+      label: Installation method
+      multiple: true
+      options:
+        - Flathub
+        - Flatpak nightly repo or download
+        - AppImage
+        - Windows download
+        - macOS DMG file
+        - Some repository (AUR, homebrew, distribution repository, PPA, etc)
+        - Local build
+  - type: input
+    id: qt-version
+    attributes:
+      label: Qt version
+      description: What version of Qt does your system use? (If you compiled Nheko yourself.)
+      placeholder: 5.15.2.
+    validations:
+      required: false
+  - type: input
+    id: compiler
+    attributes:
+      label: C++ compiler
+      description: What compiler (and version) did you use (if you compiled Nheko yourself)?
+      placeholder: gcc-9000
+    validations:
+      required: false
+  - type: input
+    id: de
+    attributes:
+      label: Desktop Environment
+      description: If you are on Linux, describe your desktop environment.
+      placeholder: KDE with i3 as the window manager
+    validations:
+      required: false
+  - type: checkboxes
+    id: profiles
+    attributes:
+      label: Did you use profiles?
+      description: Usually by passing the --profile command line parameter. If you don't know, answer 'no'.
+      options:
+        - label: Profiles used?
+          required: false
+  - type: textarea
+    id: logs
+    attributes:
+      label: Relevant log output
+      description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
+      placeholder: |
+        The log file is located in
+          Linux: ~/.cache/nheko/
+          macOS: ~/Library/Caches/nheko or /Library/Caches/nheko
+          Windows: C:/Users/<USER>/AppData/Local/nheko/cache
+      render: shell
+  - type: textarea
+    id: backtrace
+    attributes:
+      label: Backtrace
+      description: If the program crashed send a backtrace.
+      placeholder: |
+        You can retrieve a backtrace by building nheko with -DCMAKE_BUILD_TYPE=Debug and running it through gdb or lldb.
+
+        gdb ./build/nheko
+
+        >> run
+
+        ... Make the program crash
+
+        >> bt
+      render: shell
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 11fc491e..00000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: ''
-labels: enhancement
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
new file mode 100644
index 00000000..a07eff86
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -0,0 +1,49 @@
+name: Feature request
+description: Suggest an idea for this project
+labels: [enhancement]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Please verify that there is no feature request for this already!
+  - type: textarea
+    id: problem
+    attributes:
+      label: The Problem
+      description: Is your feature request related to a problem? Please describe.
+      placeholder: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
+    validations:
+      required: true
+  - type: textarea
+    id: solution
+    attributes:
+      label: The Solution
+      description: Describe the solution you'd like
+      placeholder: A clear and concise description of what you want to happen.
+    validations:
+      required: true
+  - type: textarea
+    id: alternatives
+    attributes:
+      label: Alternatives
+      description: Describe alternatives you've considered.
+      placeholder: A clear and concise description of any alternative solutions or features you've considered.
+    validations:
+      required: false
+  - type: textarea
+    id: context
+    attributes:
+      label: Additional context
+      description: Describe alternatives you've considered.
+      placeholder: Add any other context or screenshots about the feature request here.
+    validations:
+      required: false
+  - type: checkboxes
+    id: version-check
+    attributes:
+      label: Happens in the latest version
+      description: Please verify that this is still missing in the latest version.
+      options:
+        - label: Yes, this feature is still missing.
+          required: true
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 049ed8a3..8ef4470c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT)
 	FetchContent_Declare(
 		MatrixClient
 		GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-		GIT_TAG        bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
+		GIT_TAG        deb51ef1d6df870098069312f0a1999550e1eb85
 		)
 	set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
 	set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@@ -676,7 +676,7 @@ if(USE_BUNDLED_COEURL)
 	FetchContent_Declare(
 		coeurl
 		GIT_REPOSITORY https://nheko.im/Nheko-Reborn/coeurl.git
-		GIT_TAG        e9010d1ce14e7163d1cb5407ed27b23303781796
+		GIT_TAG        3901507db25cf3f9364b58cd8c7880640900c992
 		)
 	FetchContent_MakeAvailable(coeurl)
 	target_link_libraries(nheko PUBLIC coeurl::coeurl)
diff --git a/README.md b/README.md
index d442818d..1cf5d705 100644
--- a/README.md
+++ b/README.md
@@ -213,7 +213,7 @@ sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig dev-libs/qtkeychain
 
 ```bash
 # Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
-sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev
+sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev libevent-dev libcurl-dev
 ```
 This will install all dependencies, except for tweeny (use bundled tweeny)
 and mtxclient (needs to be build separately).
diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml
index a0e57b09..c9caddc8 100644
--- a/io.github.NhekoReborn.Nheko.yaml
+++ b/io.github.NhekoReborn.Nheko.yaml
@@ -152,7 +152,7 @@ modules:
       - -Ddefault_library=static
     name: coeurl
     sources:
-      - commit: 417821a07cfe4429b08a2efed5e480a498087afd
+      - commit: 3901507db25cf3f9364b58cd8c7880640900c992
         type: git
         url: https://nheko.im/nheko-reborn/coeurl.git
   - config-opts:
@@ -163,7 +163,7 @@ modules:
     buildsystem: cmake-ninja
     name: mtxclient
     sources:
-      - commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
+      - commit: deb51ef1d6df870098069312f0a1999550e1eb85
         type: git
         url: https://github.com/Nheko-Reborn/mtxclient.git
   - config-opts:
diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 9685dde1..4a9a565c 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import "./ui"
-import QtGraphicalEffects 1.0
 import QtQuick 2.6
 import QtQuick.Controls 2.3
 import im.nheko 1.0
@@ -21,7 +20,7 @@ Rectangle {
 
     width: 48
     height: 48
-    radius: Settings.avatarCircles ? height / 2 : 3
+    radius: Settings.avatarCircles ? height / 2 : height / 8
     color: Nheko.colors.alternateBase
     Component.onCompleted: {
         mouseArea.clicked.connect(clicked);
@@ -50,8 +49,7 @@ Rectangle {
         smooth: true
         sourceSize.width: avatar.width
         sourceSize.height: avatar.height
-        layer.enabled: true
-        source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
+        source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100.0 : 25.0) + ((avatar.crop) ? "" : "&scale")) : ""
 
         MouseArea {
             id: mouseArea
@@ -65,18 +63,6 @@ Rectangle {
 
         }
 
-        layer.effect: OpacityMask {
-            cached: true
-
-            maskSource: Rectangle {
-                anchors.fill: parent
-                width: avatar.width
-                height: avatar.height
-                radius: Settings.avatarCircles ? height / 2 : 3
-            }
-
-        }
-
     }
 
     Rectangle {
@@ -85,7 +71,7 @@ Rectangle {
         visible: !!userid
         height: avatar.height / 6
         width: height
-        radius: Settings.avatarCircles ? height / 2 : height / 4
+        radius: Settings.avatarCircles ? height / 2 : height / 8
         color: {
             switch (TimelineManager.userPresence(userid)) {
             case "online":
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 525477cd..26752f92 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -68,6 +68,7 @@ Popup {
             isOnlyEmoji: modelData.isOnlyEmoji ?? false
             userId: modelData.userId ?? ""
             userName: modelData.userName ?? ""
+            encryptionError: modelData.encryptionError ?? ""
         }
 
         MatrixTextField {
@@ -85,6 +86,9 @@ Popup {
                 } else if (event.key == Qt.Key_Down && completerPopup.opened) {
                     event.accepted = true;
                     completerPopup.down();
+                } else if (event.key == Qt.Key_Tab && completerPopup.opened) {
+                    event.accepted = true;
+                    completerPopup.down();
                 } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                     completerPopup.finishCompletion();
                     event.accepted = true;
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 79cbd700..e5c6b4ec 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -6,7 +6,6 @@ import "./delegates"
 import "./emoji"
 import "./ui"
 import Qt.labs.platform 1.1 as Platform
-import QtGraphicalEffects 1.0
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml
index 61155acf..defcc611 100644
--- a/resources/qml/QuickSwitcher.qml
+++ b/resources/qml/QuickSwitcher.qml
@@ -45,6 +45,9 @@ Popup {
             } else if (event.key == Qt.Key_Down && completerPopup.opened) {
                 event.accepted = true;
                 completerPopup.down();
+            } else if (event.key == Qt.Key_Tab && completerPopup.opened) {
+                event.accepted = true;
+                completerPopup.down();
             } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                 completerPopup.finishCompletion();
                 event.accepted = true;
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index 54b4f20c..e15b022f 100644
--- a/resources/qml/ReplyPopup.qml
+++ b/resources/qml/ReplyPopup.qml
@@ -45,6 +45,7 @@ Rectangle {
         isOnlyEmoji: modelData.isOnlyEmoji ?? false
         userId: modelData.userId ?? ""
         userName: modelData.userName ?? ""
+        encryptionError: modelData.encryptionError ?? ""
     }
 
     ImageButton {
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 98532606..8fbfce91 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -33,8 +33,9 @@ Page {
 
         Connections {
             function onCurrentRoomChanged() {
-                roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
-                console.log("Test" + Rooms.currentRoom.roomId + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId));
+                if (Rooms.currentRoom)
+                    roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
+
             }
 
             target: Rooms
@@ -190,7 +191,12 @@ Page {
 
                 TapHandler {
                     margin: -Nheko.paddingSmall
-                    onSingleTapped: Rooms.setCurrentRoom(roomId)
+                    onSingleTapped: {
+                        if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
+                            Rooms.setCurrentRoom(roomId);
+                        else
+                            Rooms.resetCurrentRoom();
+                    }
                     onLongPressed: {
                         if (!isInvite)
                             roomContextMenu.show(roomId, tags);
diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml
index 447e6fd1..8e44855c 100644
--- a/resources/qml/RoomMembers.qml
+++ b/resources/qml/RoomMembers.qml
@@ -13,6 +13,7 @@ ApplicationWindow {
     id: roomMembersRoot
 
     property MemberList members
+    property Room room
 
     title: qsTr("Members of %1").arg(members.roomName)
     height: 650
@@ -83,9 +84,14 @@ ApplicationWindow {
                 }
 
                 delegate: RowLayout {
+                    id: del
+
+                    width: ListView.view.width
                     spacing: Nheko.paddingMedium
 
                     Avatar {
+                        id: avatar
+
                         width: Nheko.avatarSize
                         height: Nheko.avatarSize
                         userid: model.mxid
@@ -97,16 +103,18 @@ ApplicationWindow {
                     ColumnLayout {
                         spacing: Nheko.paddingSmall
 
-                        Label {
-                            text: model.displayName
+                        ElidedLabel {
+                            fullText: model.displayName
                             color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
-                            font.pointSize: fontMetrics.font.pointSize
+                            font.pixelSize: fontMetrics.font.pixelSize
+                            elideWidth: del.width - Nheko.paddingMedium * 2 - avatar.width - encryptInd.width
                         }
 
-                        Label {
-                            text: model.mxid
+                        ElidedLabel {
+                            fullText: model.mxid
                             color: Nheko.colors.buttonText
-                            font.pointSize: fontMetrics.font.pointSize * 0.9
+                            font.pixelSize: Math.ceil(fontMetrics.font.pixelSize * 0.9)
+                            elideWidth: del.width - Nheko.paddingMedium * 2 - avatar.width - encryptInd.width
                         }
 
                         Item {
@@ -116,6 +124,28 @@ ApplicationWindow {
 
                     }
 
+                    EncryptionIndicator {
+                        id: encryptInd
+
+                        Layout.alignment: Qt.AlignRight
+                        visible: room.isEncrypted
+                        encrypted: room.isEncrypted
+                        trust: encrypted ? model.trustlevel : Crypto.Unverified
+                        ToolTip.text: {
+                            if (!encrypted)
+                                return qsTr("This room is not encrypted!");
+
+                            switch (trust) {
+                            case Crypto.Verified:
+                                return qsTr("This user is verified.");
+                            case Crypto.TOFU:
+                                return qsTr("This user isn't verified, but is still using the same master key from the first time you met.");
+                            default:
+                                return qsTr("This user has unverified devices!");
+                            }
+                        }
+                    }
+
                 }
 
                 footer: Item {
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml
index 69cf427c..491a336f 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/RoomSettings.qml
@@ -15,8 +15,8 @@ ApplicationWindow {
 
     property var roomSettings
 
-    minimumWidth: 420
-    minimumHeight: 650
+    minimumWidth: 450
+    minimumHeight: 680
     palette: Nheko.colors
     color: Nheko.colors.window
     modality: Qt.NonModal
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index b229acda..cc7d32ea 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -8,7 +8,6 @@ import "./dialogs"
 import "./emoji"
 import "./voip"
 import Qt.labs.platform 1.1 as Platform
-import QtGraphicalEffects 1.0
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
@@ -153,10 +152,10 @@ Page {
             packSet.show();
         }
 
-        function onOpenRoomMembersDialog(members) {
+        function onOpenRoomMembersDialog(members, room) {
             var membersDialog = roomMembersComponent.createObject(timelineRoot, {
                 "members": members,
-                "roomName": Rooms.currentRoom.roomName
+                "room": room
             });
             membersDialog.show();
         }
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 6fc9d51b..c8ac6bc7 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -9,7 +9,6 @@ import "./emoji"
 import "./ui"
 import "./voip"
 import Qt.labs.platform 1.1 as Platform
-import QtGraphicalEffects 1.0
 import QtQuick 2.9
 import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
@@ -85,9 +84,14 @@ Item {
                         target: timelineView
                     }
 
-                    MessageView {
+                    Loader {
+                        active: room || roomPreview
                         Layout.fillWidth: true
-                        implicitHeight: msgView.height - typingIndicator.height
+
+                        sourceComponent: MessageView {
+                            implicitHeight: msgView.height - typingIndicator.height
+                        }
+
                     }
 
                     Loader {
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 8543d02a..7f67c028 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -15,6 +15,8 @@ Rectangle {
     property string roomName: room ? room.roomName : qsTr("No room selected")
     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
 
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
@@ -92,11 +94,33 @@ Rectangle {
             text: roomTopic
         }
 
+        EncryptionIndicator {
+            Layout.column: 3
+            Layout.row: 0
+            Layout.rowSpan: 2
+            visible: isEncrypted
+            encrypted: isEncrypted
+            trust: trustlevel
+            ToolTip.text: {
+                if (!encrypted)
+                    return qsTr("This room is not encrypted!");
+
+                switch (trust) {
+                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.");
+                default:
+                    return qsTr("This room contains unverified devices!");
+                }
+            }
+        }
+
         ImageButton {
             id: roomOptionsButton
 
             visible: !!room
-            Layout.column: 3
+            Layout.column: 4
             Layout.row: 0
             Layout.rowSpan: 2
             Layout.alignment: Qt.AlignVCenter
@@ -116,7 +140,7 @@ Rectangle {
 
                 Platform.MenuItem {
                     text: qsTr("Members")
-                    onTriggered: TimelineManager.openRoomMembers(room.roomId)
+                    onTriggered: TimelineManager.openRoomMembers(room)
                 }
 
                 Platform.MenuItem {
diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml
index b839c9e3..b0f431f6 100644
--- a/resources/qml/dialogs/ImagePackEditorDialog.qml
+++ b/resources/qml/dialogs/ImagePackEditorDialog.qml
@@ -171,7 +171,7 @@ ApplicationWindow {
                     }
 
                     MatrixText {
-                        text: qsTr("Attrbution")
+                        text: qsTr("Attribution")
                     }
 
                     MatrixTextField {
diff --git a/src/Cache.cpp b/src/Cache.cpp
index ee991dc2..8b8b2985 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -114,7 +114,13 @@ ro_txn(lmdb::env &env)
                 txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
                 reuse_counter = 0;
         } else if (reuse_counter > 0) {
-                txn.renew();
+                try {
+                        txn.renew();
+                } catch (...) {
+                        txn.abort();
+                        txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+                        reuse_counter = 0;
+                }
         }
         reuse_counter++;
 
@@ -289,7 +295,9 @@ Cache::setup()
         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);
+        encryptedRooms_                      = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+        [[maybe_unused]] auto verificationDb = getVerificationDb(txn);
+        [[maybe_unused]] auto userKeysDb     = getUserKeysDb(txn);
 
         txn.commit();
 
@@ -720,20 +728,35 @@ 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->setKey("matrix." +
-                    QString(QCryptographicHash::hash(settings->profile().toUtf8(),
-                                                     QCryptographicHash::Sha256)) +
-                    "." + name.c_str());
+        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::Job::finished, job, [name, this](QKeychain::Job *job) {
-                if (job->error()) {
-                        nhlog::db()->warn(
-                          "Storing secret '{}' failed: {}", name, job->errorString().toStdString());
-                } else {
-                        emit secretChanged(name);
-                }
-        });
+
+        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();
 }
 
@@ -744,10 +767,14 @@ Cache::deleteSecret(const std::string name)
         QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
         job.setAutoDelete(false);
         job.setInsecureFallback(true);
-        job.setKey("matrix." +
-                   QString(QCryptographicHash::hash(settings->profile().toUtf8(),
-                                                    QCryptographicHash::Sha256)) +
-                   "." + name.c_str());
+        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;
@@ -765,10 +792,14 @@ Cache::secret(const std::string name)
         QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
         job.setAutoDelete(false);
         job.setInsecureFallback(true);
-        job.setKey("matrix." +
-                   QString(QCryptographicHash::hash(settings->profile().toUtf8(),
-                                                    QCryptographicHash::Sha256)) +
-                   "." + name.c_str());
+        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;
@@ -838,6 +869,9 @@ Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
 bool
 Cache::isInitialized()
 {
+        if (!env_.handle())
+                return false;
+
         auto txn = ro_txn(env_);
         std::string_view token;
 
@@ -1563,26 +1597,32 @@ Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
 RoomInfo
 Cache::singleRoomInfo(const std::string &room_id)
 {
-        auto txn      = ro_txn(env_);
-        auto statesdb = getStatesDb(txn, room_id);
+        auto txn = ro_txn(env_);
 
-        std::string_view data;
+        try {
+                auto statesdb = getStatesDb(txn, room_id);
 
-        // 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);
+                std::string_view data;
 
-                        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());
+                // 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();
@@ -3541,6 +3581,44 @@ Cache::roomMembers(const std::string &room_id)
         return members;
 }
 
+crypto::Trust
+Cache::roomVerificationStatus(const std::string &room_id)
+{
+        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;
+                }
+
+                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)
 {
@@ -3723,10 +3801,16 @@ from_json(const json &j, UserKeyCache &info)
 std::optional<UserKeyCache>
 Cache::userKeys(const std::string &user_id)
 {
+        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;
 
         try {
-                auto txn = ro_txn(env_);
                 auto db  = getUserKeysDb(txn);
                 auto res = db.get(txn, user_id, keys);
 
@@ -3735,7 +3819,8 @@ Cache::userKeys(const std::string &user_id)
                 } else {
                         return {};
                 }
-        } catch (std::exception &) {
+        } catch (std::exception &e) {
+                nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
                 return {};
         }
 }
@@ -3770,8 +3855,14 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
                         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)
+                        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) {
@@ -3819,8 +3910,43 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
                                                 }
                                         }
 
-                                        if (!keyReused && !oldDeviceKeys.count(device_id))
+                                        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) {
@@ -3830,6 +3956,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
                                 updateToWrite.seen_device_ids.insert(device_id);
                         }
                 }
+                updateToWrite.updated_at = sync_token;
                 db.put(txn, user, json(updateToWrite).dump());
         }
 
@@ -3882,14 +4009,15 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn,
                 nhlog::db()->debug("Marking user keys out of date: {}", user);
 
                 std::string_view oldKeys;
-                auto res = db.get(txn, user, oldKeys);
-
-                if (!res)
-                        continue;
 
-                auto cacheEntry =
-                  json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>();
+                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;
+
                 db.put(txn, user, json(cacheEntry).dump());
 
                 query.device_keys[user] = {};
@@ -3915,35 +4043,46 @@ void
 Cache::query_keys(const std::string &user_id,
                   std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb)
 {
-        auto cache_ = cache::userKeys(user_id);
+        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.empty() && cache_->updated_at == cache_->last_changed) {
-                        cb(cache_.value(), {});
-                        return;
-                }
-        }
+                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);
 
-        mtx::requests::QueryKeys req;
-        req.device_keys[user_id] = {};
+                req.device_keys[user_id] = {};
 
-        std::string last_changed;
-        if (cache_)
-                last_changed = cache_->last_changed;
-        req.token = last_changed;
+                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](std::string updated_user) mutable {
-                                 if (user_id == updated_user) {
-                                         context_->deleteLater();
-                                         auto keys = cache::userKeys(user_id);
-                                         cb(keys.value_or(UserKeyCache{}), {});
-                                 }
-                         });
+        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,
@@ -3971,17 +4110,16 @@ to_json(json &j, const VerificationCache &info)
 void
 from_json(const json &j, VerificationCache &info)
 {
-        info.device_verified = j.at("device_verified").get<std::vector<std::string>>();
-        info.device_blocked  = j.at("device_blocked").get<std::vector<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>>();
 }
 
 std::optional<VerificationCache>
-Cache::verificationCache(const std::string &user_id)
+Cache::verificationCache(const std::string &user_id, lmdb::txn &txn)
 {
         std::string_view verifiedVal;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getVerificationDb(txn);
+        auto db = getVerificationDb(txn);
 
         try {
                 VerificationCache verified_state;
@@ -4000,26 +4138,28 @@ Cache::verificationCache(const std::string &user_id)
 void
 Cache::markDeviceVerified(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);
+                        }
 
-                for (const auto &device : verified_state.device_verified)
-                        if (device == key)
-                                return;
+                        for (const auto &device : verified_state.device_verified)
+                                if (device == key)
+                                        return;
 
-                verified_state.device_verified.push_back(key);
-                db.put(txn, user_id, json(verified_state).dump());
-                txn.commit();
-        } catch (std::exception &) {
+                        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();
@@ -4057,11 +4197,7 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
                         verified_state = json::parse(val);
                 }
 
-                verified_state.device_verified.erase(
-                  std::remove(verified_state.device_verified.begin(),
-                              verified_state.device_verified.end(),
-                              key),
-                  verified_state.device_verified.end());
+                verified_state.device_verified.erase(key);
 
                 db.put(txn, user_id, json(verified_state).dump());
                 txn.commit();
@@ -4091,13 +4227,25 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
 VerificationStatus
 Cache::verificationStatus(const std::string &user_id)
 {
+        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);
 
         VerificationStatus status;
 
-        if (auto verifCache = verificationCache(user_id)) {
+        // 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;
         }
 
@@ -4105,12 +4253,10 @@ Cache::verificationStatus(const std::string &user_id)
 
         crypto::Trust trustlevel = crypto::Trust::Unverified;
         if (user_id == local_user) {
-                status.verified_devices.push_back(http::client()->device_id());
+                status.verified_devices.insert(http::client()->device_id());
                 trustlevel = crypto::Trust::Verified;
         }
 
-        verification_storage.status[user_id] = status;
-
         auto verifyAtLeastOneSig = [](const auto &toVerif,
                                       const std::map<std::string, std::string> &keys,
                                       const std::string &keyOwner) {
@@ -4128,6 +4274,16 @@ Cache::verificationStatus(const std::string &user_id)
                 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
@@ -4137,17 +4293,27 @@ Cache::verificationStatus(const std::string &user_id)
                 //
                 // This means verifying the other user adds 2 extra steps,verifying our user_signing
                 // key and their master key
-                auto ourKeys   = userKeys(local_user);
-                auto theirKeys = userKeys(user_id);
-                if (!ourKeys || !theirKeys)
+                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);
 
                 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())))
+                        .at("ed25519:" + http::client()->device_id()))) {
+                        verification_storage.status[user_id] = status;
                         return status;
+                }
 
                 auto master_keys = ourKeys->master_keys.keys;
 
@@ -4162,14 +4328,17 @@ Cache::verificationStatus(const std::string &user_id)
                                 trustlevel = crypto::Trust::Verified;
                         else if (!theirKeys->master_key_changed)
                                 trustlevel = crypto::Trust::TOFU;
-                        else
+                        else {
+                                verification_storage.status[user_id] = status;
                                 return status;
+                        }
 
                         master_keys = theirKeys->master_keys.keys;
                 }
 
                 status.user_verified = trustlevel;
 
+                verification_storage.status[user_id] = status;
                 if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
                         return status;
 
@@ -4180,16 +4349,19 @@ Cache::verificationStatus(const std::string &user_id)
                                   device_key.keys.at("curve25519:" + device_key.device_id);
                                 if (verifyAtLeastOneSig(
                                       device_key, theirKeys->self_signing_keys.keys, user_id)) {
-                                        status.verified_devices.push_back(device_key.device_id);
+                                        status.verified_devices.insert(device_key.device_id);
                                         status.verified_device_keys[identkey] = trustlevel;
                                 }
                         } catch (...) {
                         }
                 }
 
+                updateUnverifiedDevices(theirKeys->device_keys);
                 verification_storage.status[user_id] = status;
                 return status;
-        } catch (std::exception &) {
+        } catch (std::exception &e) {
+                nhlog::db()->error(
+                  "Failed to calculate verification status of {}: {}", user_id, e.what());
                 return status;
         }
 }
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 69d64885..6c402674 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -112,9 +112,13 @@ 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::vector<std::string> verified_devices;
+        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
@@ -154,9 +158,9 @@ from_json(const nlohmann::json &j, UserKeyCache &info);
 struct VerificationCache
 {
         //! list of verified device_ids with device-verification
-        std::vector<std::string> device_verified;
+        std::set<std::string> device_verified;
         //! list of devices the user blocks
-        std::vector<std::string> device_blocked;
+        std::set<std::string> device_blocked;
 };
 
 void
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 30c365a6..748404d1 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -46,7 +46,6 @@ public:
         std::string statusMessage(const std::string &user_id);
 
         // user cache stores user keys
-        std::optional<UserKeyCache> userKeys(const std::string &user_id);
         std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
           const std::string &room_id,
           bool verified_only);
@@ -63,9 +62,11 @@ public:
                         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();
 
@@ -414,24 +415,25 @@ private:
                           if constexpr (isStateEvent_<decltype(e)>) {
                                   eventsDb.put(txn, e.event_id, json(e).dump());
 
-                                  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.type != EventType::Unsupported) {
-                                          if (e.state_key.empty())
+                                  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
@@ -680,7 +682,10 @@ private:
                 return QString::fromStdString(event.state_key);
         }
 
-        std::optional<VerificationCache> verificationCache(const std::string &user_id);
+        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);
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 42e3bc7b..8a0e891b 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -4,11 +4,9 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 #include <QApplication>
-#include <QImageReader>
 #include <QInputDialog>
 #include <QMessageBox>
 #include <QSettings>
-#include <QShortcut>
 
 #include <mtx/responses.hpp>
 
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 751e7074..c90b87f5 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -17,10 +17,8 @@
 #include <mtx/events/presence.hpp>
 #include <mtx/secret_storage.hpp>
 
-#include <QFrame>
 #include <QHBoxLayout>
 #include <QMap>
-#include <QPixmap>
 #include <QPoint>
 #include <QTimer>
 #include <QWidget>
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
index 196647fe..0c0f0cdd 100644
--- a/src/MemberList.cpp
+++ b/src/MemberList.cpp
@@ -53,6 +53,7 @@ MemberList::roleNames() const
           {Mxid, "mxid"},
           {DisplayName, "displayName"},
           {AvatarUrl, "avatarUrl"},
+          {Trustlevel, "trustlevel"},
         };
 }
 
@@ -69,6 +70,17 @@ MemberList::data(const QModelIndex &index, int role) const
                 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 {};
         }
diff --git a/src/MemberList.h b/src/MemberList.h
index e6522694..cffcd83d 100644
--- a/src/MemberList.h
+++ b/src/MemberList.h
@@ -25,6 +25,7 @@ public:
                 Mxid,
                 DisplayName,
                 AvatarUrl,
+                Trustlevel,
         };
         MemberList(const QString &room_id, QObject *parent = nullptr);
 
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index b8648269..056374a9 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -11,6 +11,8 @@
 #include <QByteArray>
 #include <QDir>
 #include <QFileInfo>
+#include <QPainter>
+#include <QPainterPath>
 #include <QStandardPaths>
 
 #include "Logging.h"
@@ -22,14 +24,26 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
 QQuickImageResponse *
 MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
 {
-        auto id_  = id;
-        bool crop = true;
-        if (id.endsWith("?scale")) {
-                crop = false;
-                id_.remove("?scale");
+        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, requestedSize);
+        MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize);
         pool.start(response);
         return response;
 }
@@ -53,14 +67,35 @@ MxcImageResponse::run()
                   }
                   emit finished();
           },
-          m_crop);
+          m_crop,
+          m_radius);
+}
+
+static QImage
+clipRadius(QImage img, double radius)
+{
+        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);
+
+        QPainterPath ppath;
+        ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
+
+        painter.setClipPath(ppath);
+        painter.drawImage(img.rect(), img);
+
+        return out;
 }
 
 void
 MxcImageProvider::download(const QString &id,
                            const QSize &requestedSize,
                            std::function<void(QString, QSize, QImage, QString)> then,
-                           bool crop)
+                           bool crop,
+                           double radius)
 {
         std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
         auto temp = infos.find("mxc://" + id);
@@ -69,12 +104,13 @@ MxcImageProvider::download(const QString &id,
 
         if (requestedSize.isValid() && !encryptionInfo) {
                 QString fileName =
-                  QString("%1_%2x%3_%4")
+                  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(crop ? "crop" : "scale")
+                    .arg(radius);
                 QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
                                      "/media_cache",
                                    fileName);
@@ -86,6 +122,10 @@ MxcImageProvider::download(const QString &id,
                                 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;
@@ -100,8 +140,8 @@ MxcImageProvider::download(const QString &id,
                 opts.method  = crop ? "crop" : "scale";
                 http::client()->get_thumbnail(
                   opts,
-                  [fileInfo, requestedSize, then, id](const std::string &res,
-                                                      mtx::http::RequestErr err) {
+                  [fileInfo, requestedSize, radius, then, id](const std::string &res,
+                                                              mtx::http::RequestErr err) {
                           if (err || res.empty()) {
                                   then(id, QSize(), {}, "");
 
@@ -113,6 +153,10 @@ MxcImageProvider::download(const QString &id,
                           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"))
@@ -126,8 +170,12 @@ MxcImageProvider::download(const QString &id,
                   });
         } else {
                 try {
-                        QString fileName = QString::fromUtf8(id.toUtf8().toBase64(
-                          QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
+                        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",
@@ -148,6 +196,11 @@ MxcImageProvider::download(const QString &id,
                                         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,
@@ -158,6 +211,11 @@ MxcImageProvider::download(const QString &id,
                                         QImage image =
                                           utils::readImageFromFile(fileInfo.absoluteFilePath());
                                         if (!image.isNull()) {
+                                                if (radius != 0) {
+                                                        image =
+                                                          clipRadius(std::move(image), radius);
+                                                }
+
                                                 then(id,
                                                      requestedSize,
                                                      image,
@@ -169,7 +227,7 @@ MxcImageProvider::download(const QString &id,
 
                         http::client()->download(
                           "mxc://" + id.toStdString(),
-                          [fileInfo, requestedSize, then, id, encryptionInfo](
+                          [fileInfo, requestedSize, then, id, radius, encryptionInfo](
                             const std::string &res,
                             const std::string &,
                             const std::string &originalFilename,
@@ -195,6 +253,10 @@ MxcImageProvider::download(const QString &id,
                                           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);
@@ -205,6 +267,10 @@ MxcImageProvider::download(const QString &id,
 
                                   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);
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
index 61d82852..6de83c0e 100644
--- a/src/MxcImageProvider.h
+++ b/src/MxcImageProvider.h
@@ -19,10 +19,11 @@ class MxcImageResponse
   , public QRunnable
 {
 public:
-        MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
+        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);
         }
@@ -39,6 +40,7 @@ public:
         QSize m_requestedSize;
         QImage m_image;
         bool m_crop;
+        double m_radius;
 };
 
 class MxcImageProvider
@@ -54,7 +56,8 @@ public slots:
         static void download(const QString &id,
                              const QSize &requestedSize,
                              std::function<void(QString, QSize, QImage, QString)> then,
-                             bool crop = true);
+                             bool crop     = true,
+                             double radius = 0);
 
 private:
         QThreadPool pool;
diff --git a/src/Olm.cpp b/src/Olm.cpp
index e4ab0aa1..2c9ac5a3 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -425,6 +425,8 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
                                                     }
                                             });
 
+                                        nhlog::crypto()->info("Storing secret {}",
+                                                              secret_name->second);
                                         cache::client()->storeSecret(secret_name->second,
                                                                      e->content.secret);
 
@@ -1110,6 +1112,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
                                   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;
@@ -1162,7 +1166,6 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
 
                         auto session = cache::getLatestOlmSession(device_curve);
                         if (!session || force_new_session) {
-                                static QMap<QPair<std::string, std::string>, qint64> rateLimit;
                                 auto currentTime = QDateTime::currentSecsSinceEpoch();
                                 if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
                                     currentTime) {
@@ -1318,7 +1321,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
                 };
         };
 
-        http::client()->claim_keys(claims, BindPks(pks));
+        if (!claims.one_time_keys.empty())
+                http::client()->claim_keys(claims, BindPks(pks));
 
         if (!keysToQuery.empty()) {
                 mtx::requests::QueryKeys req;
@@ -1395,9 +1399,25 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
                                                   continue;
                                           }
 
-                                          deviceKeys[user_id].emplace(device_id, pks);
-                                          claim_keys.one_time_keys[user.first][device_id] =
-                                            mtx::crypto::SIGNED_CURVE25519;
+                                          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);
@@ -1405,7 +1425,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
                                   }
                           }
 
-                          http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
+                          if (!claim_keys.one_time_keys.empty())
+                                  http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
                   });
         }
 }
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index bae24df0..fb6a1b97 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -3,6 +3,7 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+#include <QInputDialog>
 #include <QLabel>
 #include <QMetaType>
 #include <QPainter>
@@ -481,6 +482,23 @@ RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
                 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(
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index ab6ac492..f67c5e2d 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -19,7 +19,6 @@
 #include <QResizeEvent>
 #include <QScrollArea>
 #include <QScroller>
-#include <QSettings>
 #include <QSpinBox>
 #include <QStandardPaths>
 #include <QString>
@@ -63,7 +62,6 @@ UserSettings::initialize(std::optional<QString> profile)
 void
 UserSettings::load(std::optional<QString> profile)
 {
-        QSettings settings;
         tray_        = settings.value("user/window/tray", false).toBool();
         startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
 
@@ -601,7 +599,6 @@ UserSettings::applyTheme()
 void
 UserSettings::save()
 {
-        QSettings settings;
         settings.beginGroup("user");
 
         settings.beginGroup("window");
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 096aab81..84940e47 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -8,6 +8,7 @@
 #include <QFontDatabase>
 #include <QFrame>
 #include <QProcessEnvironment>
+#include <QSettings>
 #include <QSharedPointer>
 #include <QWidget>
 
@@ -107,6 +108,8 @@ public:
         static QSharedPointer<UserSettings> instance();
         static void initialize(std::optional<QString> profile);
 
+        QSettings *qsettings() { return &settings; }
+
         enum class Presence
         {
                 AutomaticPresence,
@@ -316,6 +319,8 @@ private:
         QString homeserver_;
         QStringList hiddenTags_;
 
+        QSettings settings;
+
         static QSharedPointer<UserSettings> instance_;
 };
 
diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index f38b29f5..12813d57 100644
--- a/src/dialogs/ImageOverlay.cpp
+++ b/src/dialogs/ImageOverlay.cpp
@@ -28,8 +28,10 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
         setAttribute(Qt::WA_TranslucentBackground, true);
         setAttribute(Qt::WA_DeleteOnClose, true);
         setWindowState(Qt::WindowFullScreen);
+        close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this);
 
-        connect(this, SIGNAL(closing()), this, SLOT(close()));
+        connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing);
+        connect(this, &ImageOverlay::closing, this, &ImageOverlay::close);
 
         raise();
 }
diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h
index 93b6afdc..9d4187bf 100644
--- a/src/dialogs/ImageOverlay.h
+++ b/src/dialogs/ImageOverlay.h
@@ -8,6 +8,7 @@
 #include <QDialog>
 #include <QMouseEvent>
 #include <QPixmap>
+#include <QShortcut>
 
 namespace dialogs {
 
@@ -32,5 +33,6 @@ private:
         QRect content_;
         QRect close_button_;
         QRect save_button_;
+        QShortcut *close_shortcut_;
 };
 } // dialogs
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 99e00a67..79c28edf 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -310,7 +310,7 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
                 return mtx::events::EventType::RoomMessage;
                 //! m.image_pack, currently im.ponies.room_emotes
         case qml_mtx_events::ImagePackInRoom:
-                return mtx::events::EventType::ImagePackRooms;
+                return mtx::events::EventType::ImagePackInRoom;
         //! m.image_pack, currently im.ponies.user_emotes
         case qml_mtx_events::ImagePackInAccountData:
                 return mtx::events::EventType::ImagePackInAccountData;
@@ -418,6 +418,14 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
                 &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);
 }
 
@@ -1993,6 +2001,15 @@ TimelineModel::roomTopic() const
                   QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
 }
 
+crypto::Trust
+TimelineModel::trustlevel() const
+{
+        if (!isEncrypted_)
+                return crypto::Trust::Unverified;
+
+        return cache::client()->roomVerificationStatus(room_id_.toStdString());
+}
+
 int
 TimelineModel::roomMemberCount() const
 {
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index ad7cfbbb..aa07fe01 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -175,6 +175,7 @@ class TimelineModel : public QAbstractListModel
         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)
 
@@ -287,6 +288,7 @@ public:
         DescInfo lastMessage() const { return lastMessage_; }
         bool isSpace() const { return isSpace_; }
         bool isEncrypted() const { return isEncrypted_; }
+        crypto::Trust trustlevel() const;
         int roomMemberCount() const;
 
 public slots:
@@ -372,6 +374,7 @@ signals:
         void updateFlowEventId(std::string event_id);
 
         void encryptionChanged();
+        void trustlevelChanged();
         void roomNameChanged();
         void plainRoomNameChanged();
         void roomTopicChanged();
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b23ed278..906e328f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -375,10 +375,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
 }
 
 void
-TimelineViewManager::openRoomMembers(QString room_id)
+TimelineViewManager::openRoomMembers(TimelineModel *room)
 {
-        MemberList *memberList = new MemberList(room_id, this);
-        emit openRoomMembersDialog(memberList);
+        if (!room)
+                return;
+        MemberList *memberList = new MemberList(room->roomId(), this);
+        emit openRoomMembersDialog(memberList, room);
 }
 
 void
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 54e3a935..4dd5e996 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -66,7 +66,7 @@ public:
         Q_INVOKABLE QString userPresence(QString id) const;
         Q_INVOKABLE QString userStatus(QString id) const;
 
-        Q_INVOKABLE void openRoomMembers(QString room_id);
+        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);
@@ -92,7 +92,7 @@ signals:
         void focusChanged();
         void focusInput();
         void openImageOverlayInternalCb(QString eventId, QImage img);
-        void openRoomMembersDialog(MemberList *members);
+        void openRoomMembersDialog(MemberList *members, TimelineModel *room);
         void openRoomSettingsDialog(RoomSettings *settings);
         void openInviteUsersDialog(InviteesModel *invitees);
         void openProfile(UserProfile *profile);