summary refs log tree commit diff
diff options
context:
space:
mode:
authorkamathmanu <manuriddle@gmail.com>2021-08-07 21:20:43 +0000
committerGitHub <noreply@github.com>2021-08-07 21:20:43 +0000
commit2dfccda73c44d97e9e3e52db3e03315e8ef656a5 (patch)
tree52c9855610cbdf6f6284cfacbaba07f676a0f621
parentFix Duplicate fetched chunk (diff)
parentShow encryption errors in qml and add request keys button (diff)
downloadnheko-2dfccda73c44d97e9e3e52db3e03315e8ef656a5.tar.xz
Merge branch 'master' into nhekoRoomDirectory
-rw-r--r--.gitlab-ci.yml18
-rw-r--r--CMakeLists.txt19
-rw-r--r--io.github.NhekoReborn.Nheko.yaml4
-rw-r--r--resources/qml/Avatar.qml6
-rw-r--r--resources/qml/InviteDialog.qml4
-rw-r--r--resources/qml/MessageInput.qml2
-rw-r--r--resources/qml/MessageView.qml10
-rw-r--r--resources/qml/RawMessageDialog.qml52
-rw-r--r--resources/qml/ReadReceipts.qml130
-rw-r--r--resources/qml/RoomList.qml44
-rw-r--r--resources/qml/RoomMembers.qml6
-rw-r--r--resources/qml/RoomSettings.qml11
-rw-r--r--resources/qml/Root.qml22
-rw-r--r--resources/qml/ScrollHelper.qml6
-rw-r--r--resources/qml/StatusIndicator.qml2
-rw-r--r--resources/qml/TimelineRow.qml5
-rw-r--r--resources/qml/TimelineView.qml21
-rw-r--r--resources/qml/UserProfile.qml14
-rw-r--r--resources/qml/components/AvatarListTile.qml133
-rw-r--r--resources/qml/delegates/Encrypted.qml48
-rw-r--r--resources/qml/delegates/MessageDelegate.qml11
-rw-r--r--resources/qml/delegates/Reply.qml4
-rw-r--r--resources/qml/device-verification/DeviceVerification.qml7
-rw-r--r--resources/qml/dialogs/ImagePackEditorDialog.qml301
-rw-r--r--resources/qml/dialogs/ImagePackSettingsDialog.qml201
-rw-r--r--resources/qml/dialogs/InputDialog.qml1
-rw-r--r--resources/res.qrc13
-rw-r--r--src/Cache.cpp62
-rw-r--r--src/Cache_p.h39
-rw-r--r--src/ChatPage.cpp1
-rw-r--r--src/ImagePackListModel.cpp18
-rw-r--r--src/ImagePackListModel.h4
-rw-r--r--src/MainWindow.cpp22
-rw-r--r--src/MainWindow.h1
-rw-r--r--src/MemberList.cpp11
-rw-r--r--src/MemberList.h3
-rw-r--r--src/MxcImageProvider.cpp26
-rw-r--r--src/MxcImageProvider.h7
-rw-r--r--src/Olm.cpp41
-rw-r--r--src/Olm.h9
-rw-r--r--src/ReadReceiptsModel.cpp131
-rw-r--r--src/ReadReceiptsModel.h73
-rw-r--r--src/RegisterPage.cpp548
-rw-r--r--src/RegisterPage.h36
-rw-r--r--src/SingleImagePackModel.cpp250
-rw-r--r--src/SingleImagePackModel.h46
-rw-r--r--src/UserSettingsPage.cpp113
-rw-r--r--src/UserSettingsPage.h7
-rw-r--r--src/dialogs/RawMessage.h60
-rw-r--r--src/dialogs/ReadReceipts.cpp179
-rw-r--r--src/dialogs/ReadReceipts.h61
-rw-r--r--src/timeline/EventStore.cpp256
-rw-r--r--src/timeline/EventStore.h9
-rw-r--r--src/timeline/RoomlistModel.cpp2
-rw-r--r--src/timeline/TimelineModel.cpp41
-rw-r--r--src/timeline/TimelineModel.h20
-rw-r--r--src/timeline/TimelineViewManager.cpp8
-rw-r--r--src/ui/Avatar.cpp168
-rw-r--r--src/ui/Avatar.h48
-rw-r--r--src/ui/NhekoGlobalObject.cpp7
-rw-r--r--src/ui/NhekoGlobalObject.h8
61 files changed, 2098 insertions, 1312 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7ff92c17..cea6be7b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,14 +52,14 @@ build-macos:
   stage: build
   tags: [macos]
   before_script:
-    - brew update
-    - brew reinstall --force python3
-    - brew bundle --file=./.ci/macos/Brewfile --force --cleanup
+    #- 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/bin/:${PATH}
-    - export CMAKE_PREFIX_PATH=/usr/local/opt/qt5
+    - export PATH=/usr/local/opt/qt@5/bin/:${PATH}
+    - export CMAKE_PREFIX_PATH=/usr/local/opt/qt@5
     - cmake -GNinja -H. -Bbuild
         -DCMAKE_BUILD_TYPE=RelWithDebInfo
         -DCMAKE_INSTALL_PREFIX=.deps/usr
@@ -91,7 +91,9 @@ build-flatpak-amd64:
   #image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
   tags: [docker]
   before_script:
-    - apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
+    # need flatpak 1.11.1 at least
+    - apt-get update && apt-get install -y software-properties-common
+    - add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
     - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
     - flatpak --noninteractive install --user flathub org.kde.Platform//5.15
     - flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
@@ -119,7 +121,9 @@ build-flatpak-arm64:
   #image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
   tags: [docker-arm64]
   before_script:
-    - apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
+    # need flatpak 1.11.1 at least
+    - apt-get update && apt-get install -y software-properties-common
+    - add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
     - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
     - flatpak --noninteractive install --user flathub org.kde.Platform//5.15
     - flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 587059fe..bcf31b41 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -286,7 +286,6 @@ set(SRC_FILES
 	src/dialogs/Logout.cpp
 	src/dialogs/PreviewUploadOverlay.cpp
 	src/dialogs/ReCaptcha.cpp
-	src/dialogs/ReadReceipts.cpp
 
 	# Emoji
 	src/emoji/EmojiModel.cpp
@@ -305,7 +304,6 @@ set(SRC_FILES
 	src/timeline/RoomlistModel.cpp
 
 	# UI components
-	src/ui/Avatar.cpp
 	src/ui/Badge.cpp
 	src/ui/DropShadow.cpp
 	src/ui/FlatButton.cpp
@@ -352,6 +350,7 @@ set(SRC_FILES
 	src/MemberList.cpp
 	src/MxcImageProvider.cpp
 	src/Olm.cpp
+    src/ReadReceiptsModel.cpp
 	src/RegisterPage.cpp
 	src/SSOHandler.cpp
 	src/CombinedImagePackModel.cpp
@@ -383,7 +382,7 @@ if(USE_BUNDLED_MTXCLIENT)
 	FetchContent_Declare(
 		MatrixClient
 		GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-		GIT_TAG        316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
+		GIT_TAG        bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
 		)
 	set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
 	set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@@ -498,9 +497,7 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/dialogs/LeaveRoom.h
 	src/dialogs/Logout.h
 	src/dialogs/PreviewUploadOverlay.h
-	src/dialogs/RawMessage.h
 	src/dialogs/ReCaptcha.h
-	src/dialogs/ReadReceipts.h
 
 	# Emoji
 	src/emoji/EmojiModel.h
@@ -518,7 +515,6 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/timeline/RoomlistModel.h
 
 	# UI components
-	src/ui/Avatar.h
 	src/ui/Badge.h
 	src/ui/FlatButton.h
 	src/ui/FloatingButton.h
@@ -546,24 +542,26 @@ qt5_wrap_cpp(MOC_HEADERS
 
 	src/AvatarProvider.h
 	src/BlurhashProvider.h
-	src/Cache_p.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/LoginPage.h
 	src/MainWindow.h
 	src/MemberList.h
 	src/MxcImageProvider.h
+	src/Olm.h
 	src/RegisterPage.h
+	src/RoomsModel.h
 	src/SSOHandler.h
-	src/CombinedImagePackModel.h
 	src/SingleImagePackModel.h
-	src/ImagePackListModel.h
 	src/TrayIcon.h
 	src/UserSettingsPage.h
 	src/UsersModel.h
@@ -571,7 +569,8 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/RoomDirectoryModel.h
 	src/WebRTCSession.h
 	src/WelcomePage.h
-	)
+	src/ReadReceiptsModel.h
+)
 
 #
 # Bundle translations.
diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml
index 0fa450b3..a0e57b09 100644
--- a/io.github.NhekoReborn.Nheko.yaml
+++ b/io.github.NhekoReborn.Nheko.yaml
@@ -19,6 +19,8 @@ finish-args:
   - --talk-name=org.freedesktop.secrets
   - --talk-name=org.freedesktop.StatusNotifierItem
   - --talk-name=org.kde.*
+  # needed for SingleApplication to work
+  - --allow=per-app-dev-shm
 cleanup:
   - /include
   - /bin/mdb*
@@ -161,7 +163,7 @@ modules:
     buildsystem: cmake-ninja
     name: mtxclient
     sources:
-      - commit: 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
+      - commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
         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 6c12952a..9685dde1 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -11,10 +11,11 @@ import im.nheko 1.0
 Rectangle {
     id: avatar
 
-    property alias url: img.source
+    property string url
     property string userid
     property string displayName
     property alias textColor: label.color
+    property bool crop: true
 
     signal clicked(var mouse)
 
@@ -44,12 +45,13 @@ Rectangle {
 
         anchors.fill: parent
         asynchronous: true
-        fillMode: Image.PreserveAspectCrop
+        fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit
         mipmap: true
         smooth: true
         sourceSize.width: avatar.width
         sourceSize.height: avatar.height
         layer.enabled: true
+        source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
 
         MouseArea {
             id: mouseArea
diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml
index 50287ad5..2c0e15a7 100644
--- a/resources/qml/InviteDialog.qml
+++ b/resources/qml/InviteDialog.qml
@@ -30,12 +30,12 @@ ApplicationWindow {
     }
 
     title: qsTr("Invite users to %1").arg(plainRoomName)
-    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
-    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
     height: 380
     width: 340
     palette: Nheko.colors
     color: Nheko.colors.window
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(inviteDialogRoot)
 
     Shortcut {
         sequence: "Ctrl+Enter"
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 8bc8ac62..7fb09684 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -7,7 +7,7 @@ import "./voip"
 import QtQuick 2.12
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
-import QtQuick.Window 2.2
+import QtQuick.Window 2.13
 import im.nheko 1.0
 
 Rectangle {
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 07feec8c..79cbd700 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -10,7 +10,7 @@ import QtGraphicalEffects 1.0
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
-import QtQuick.Window 2.2
+import QtQuick.Window 2.13
 import im.nheko 1.0
 
 ScrollView {
@@ -212,9 +212,9 @@ ScrollView {
 
             // force current read index to update
             onTriggered: {
-                if (chat.model) {
+                if (chat.model)
                     chat.model.setCurrentIndex(chat.model.currentIndex);
-                }
+
             }
             interval: 1000
         }
@@ -349,6 +349,7 @@ ScrollView {
             required property string callType
             required property var reactions
             required property int trustlevel
+            required property int encryptionError
             required property var timestamp
             required property int status
             required property int index
@@ -456,6 +457,7 @@ ScrollView {
                 callType: wrapper.callType
                 reactions: wrapper.reactions
                 trustlevel: wrapper.trustlevel
+                encryptionError: wrapper.encryptionError
                 timestamp: wrapper.timestamp
                 status: wrapper.status
                 relatedEventCacheBuster: wrapper.relatedEventCacheBuster
@@ -580,7 +582,7 @@ ScrollView {
 
         Platform.MenuItem {
             text: qsTr("Read receip&ts")
-            onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
+            onTriggered: room.showReadReceipts(messageContextMenu.eventId)
         }
 
         Platform.MenuItem {
diff --git a/resources/qml/RawMessageDialog.qml b/resources/qml/RawMessageDialog.qml
new file mode 100644
index 00000000..e2a476cd
--- /dev/null
+++ b/resources/qml/RawMessageDialog.qml
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: rawMessageRoot
+
+    property alias rawMessage: rawMessageView.text
+
+    height: 420
+    width: 420
+    palette: Nheko.colors
+    color: Nheko.colors.window
+    flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(rawMessageRoot)
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: rawMessageRoot.close()
+    }
+
+    ScrollView {
+        anchors.margins: Nheko.paddingMedium
+        anchors.fill: parent
+        palette: Nheko.colors
+        padding: Nheko.paddingMedium
+
+        TextArea {
+            id: rawMessageView
+
+            font: Nheko.monospaceFont()
+            color: Nheko.colors.text
+            readOnly: true
+
+            background: Rectangle {
+                color: Nheko.colors.base
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok
+        onAccepted: rawMessageRoot.close()
+    }
+
+}
diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml
new file mode 100644
index 00000000..9adbfd5c
--- /dev/null
+++ b/resources/qml/ReadReceipts.qml
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: readReceiptsRoot
+
+    property ReadReceiptsProxy readReceipts
+    property Room room
+
+    height: 380
+    width: 340
+    minimumHeight: 380
+    minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium
+    palette: Nheko.colors
+    color: Nheko.colors.window
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(readReceiptsRoot)
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: readReceiptsRoot.close()
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        anchors.margins: Nheko.paddingMedium
+        spacing: Nheko.paddingMedium
+
+        Label {
+            id: headerTitle
+
+            color: Nheko.colors.text
+            Layout.alignment: Qt.AlignCenter
+            text: qsTr("Read receipts")
+            font.pointSize: fontMetrics.font.pointSize * 1.5
+        }
+
+        ScrollView {
+            palette: Nheko.colors
+            padding: Nheko.paddingMedium
+            ScrollBar.horizontal.visible: false
+            Layout.fillHeight: true
+            Layout.minimumHeight: 200
+            Layout.fillWidth: true
+
+            ListView {
+                id: readReceiptsList
+
+                clip: true
+                spacing: Nheko.paddingMedium
+                boundsBehavior: Flickable.StopAtBounds
+                model: readReceipts
+
+                delegate: RowLayout {
+                    spacing: Nheko.paddingMedium
+
+                    Avatar {
+                        width: Nheko.avatarSize
+                        height: Nheko.avatarSize
+                        userid: model.mxid
+                        url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                        displayName: model.displayName
+                        onClicked: room.openUserProfile(model.mxid)
+                        ToolTip.visible: avatarHover.hovered
+                        ToolTip.text: model.mxid
+
+                        HoverHandler {
+                            id: avatarHover
+                        }
+
+                    }
+
+                    ColumnLayout {
+                        spacing: Nheko.paddingSmall
+
+                        Label {
+                            text: model.displayName
+                            color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
+                            font.pointSize: fontMetrics.font.pointSize
+                            ToolTip.visible: displayNameHover.hovered
+                            ToolTip.text: model.mxid
+
+                            TapHandler {
+                                onSingleTapped: room.openUserProfile(userId)
+                            }
+
+                            CursorShape {
+                                anchors.fill: parent
+                                cursorShape: Qt.PointingHandCursor
+                            }
+
+                            HoverHandler {
+                                id: displayNameHover
+                            }
+
+                        }
+
+                        Label {
+                            text: model.timestamp
+                            color: Nheko.colors.buttonText
+                            font.pointSize: fontMetrics.font.pointSize * 0.9
+                        }
+
+                        Item {
+                            Layout.fillHeight: true
+                            Layout.fillWidth: true
+                        }
+
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok
+        onAccepted: readReceiptsRoot.close()
+    }
+
+}
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 31c9d3cf..e8aacf75 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -179,31 +179,38 @@ Component {
                 }
             ]
 
-            TapHandler {
-                margin: -Nheko.paddingSmall
-                acceptedButtons: Qt.RightButton
-                onSingleTapped: {
-                    if (!TimelineManager.isInvite)
-                        roomContextMenu.show(roomId, tags);
+            // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that...
+            Item {
+                anchors.fill: parent
+                anchors.margins: 1
 
+                TapHandler {
+                    acceptedButtons: Qt.RightButton
+                    onSingleTapped: {
+                        if (!TimelineManager.isInvite)
+                            roomContextMenu.show(roomId, tags);
+
+                    }
+                    gesturePolicy: TapHandler.ReleaseWithinBounds
+                    acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
                 }
-                gesturePolicy: TapHandler.ReleaseWithinBounds
-            }
 
-            TapHandler {
-                margin: -Nheko.paddingSmall
-                onSingleTapped: Rooms.setCurrentRoom(roomId)
-                onLongPressed: {
-                    if (!isInvite)
-                        roomContextMenu.show(roomId, tags);
+                TapHandler {
+                    margin: -Nheko.paddingSmall
+                    onSingleTapped: Rooms.setCurrentRoom(roomId)
+                    onLongPressed: {
+                        if (!isInvite)
+                            roomContextMenu.show(roomId, tags);
 
+                    }
                 }
-            }
 
-            HoverHandler {
-                id: hovered
+                HoverHandler {
+                    id: hovered
+
+                    acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+                }
 
-                margin: -Nheko.paddingSmall
             }
 
             RowLayout {
@@ -439,6 +446,7 @@ Component {
                     url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
                     displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
                     userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
+                    enabled: false
                 }
 
                 ColumnLayout {
diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml
index 641a08be..447e6fd1 100644
--- a/resources/qml/RoomMembers.qml
+++ b/resources/qml/RoomMembers.qml
@@ -6,7 +6,7 @@ import "./ui"
 import QtQuick 2.12
 import QtQuick.Controls 2.12
 import QtQuick.Layouts 1.12
-import QtQuick.Window 2.12
+import QtQuick.Window 2.13
 import im.nheko 1.0
 
 ApplicationWindow {
@@ -15,13 +15,13 @@ ApplicationWindow {
     property MemberList members
 
     title: qsTr("Members of %1").arg(members.roomName)
-    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
-    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
     height: 650
     width: 420
     minimumHeight: 420
     palette: Nheko.colors
     color: Nheko.colors.window
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(roomMembersRoot)
 
     Shortcut {
         sequence: StandardKey.Cancel
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml
index b8e527a5..69cf427c 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/RoomSettings.qml
@@ -7,7 +7,7 @@ import Qt.labs.platform 1.1 as Platform
 import QtQuick 2.15
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
-import QtQuick.Window 2.3
+import QtQuick.Window 2.13
 import im.nheko 1.0
 
 ApplicationWindow {
@@ -15,14 +15,13 @@ ApplicationWindow {
 
     property var roomSettings
 
-    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
-    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
     minimumWidth: 420
     minimumHeight: 650
     palette: Nheko.colors
     color: Nheko.colors.window
     modality: Qt.NonModal
-    flags: Qt.Dialog
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(roomSettingsDialog)
     title: qsTr("Room Settings")
 
     Shortcut {
@@ -155,7 +154,7 @@ ApplicationWindow {
 
         GridLayout {
             columns: 2
-            rowSpacing: 10
+            rowSpacing: Nheko.paddingLarge
 
             MatrixText {
                 text: qsTr("SETTINGS")
@@ -181,7 +180,7 @@ ApplicationWindow {
             }
 
             MatrixText {
-                text: "Room access"
+                text: qsTr("Room access")
                 Layout.fillWidth: true
             }
 
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index e80ff764..b229acda 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -9,10 +9,10 @@ import "./emoji"
 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 2.15
+import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
-import QtQuick.Window 2.2
+import QtQuick.Window 2.15
 import im.nheko 1.0
 import im.nheko.EmojiModel 1.0
 
@@ -96,6 +96,22 @@ Page {
 
     }
 
+    Component {
+        id: readReceiptsDialog
+
+        ReadReceipts {
+        }
+
+    }
+
+    Component {
+        id: rawMessageDialog
+
+        RawMessageDialog {
+        }
+
+    }
+
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml
index 2dd56f27..e84e67fd 100644
--- a/resources/qml/ScrollHelper.qml
+++ b/resources/qml/ScrollHelper.qml
@@ -23,6 +23,9 @@ MouseArea {
     // console.warn("Delta: ", wheel.pixelDelta.y);
     // console.warn("Old position: ", flickable.contentY);
     // console.warn("New position: ", newPos);
+    // breaks ListView's with headers...
+    //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
+    //    minYExtent += flickableItem.headerItem.height;
 
     id: root
 
@@ -55,9 +58,6 @@ MouseArea {
 
         var minYExtent = flickableItem.originY + flickableItem.topMargin;
         var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
-        if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
-            minYExtent += flickableItem.headerItem.height;
-
         //Avoid overscrolling
         return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
     }
diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml
index 7e471d69..0af02b3c 100644
--- a/resources/qml/StatusIndicator.qml
+++ b/resources/qml/StatusIndicator.qml
@@ -34,7 +34,7 @@ ImageButton {
     }
     onClicked: {
         if (status == MtxEvent.Read)
-            room.readReceiptsAction(eventId);
+            room.showReadReceipts(eventId);
 
     }
     image: {
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 755ab503..c612479a 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -7,7 +7,7 @@ import "./emoji"
 import QtQuick 2.12
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
-import QtQuick.Window 2.2
+import QtQuick.Window 2.13
 import im.nheko 1.0
 
 Item {
@@ -38,6 +38,7 @@ Item {
     required property string callType
     required property var reactions
     required property int trustlevel
+    required property int encryptionError
     required property var timestamp
     required property int status
     required property int relatedEventCacheBuster
@@ -110,6 +111,7 @@ Item {
                 roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
                 roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
                 callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
+                encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? ""
                 relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
             }
 
@@ -136,6 +138,7 @@ Item {
                 roomTopic: r.roomTopic
                 roomName: r.roomName
                 callType: r.callType
+                encryptionError: r.encryptionError
                 relatedEventCacheBuster: r.relatedEventCacheBuster
                 isReply: false
             }
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index c5cc69a6..6fc9d51b 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -13,7 +13,7 @@ import QtGraphicalEffects 1.0
 import QtQuick 2.9
 import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
-import QtQuick.Window 2.2
+import QtQuick.Window 2.13
 import im.nheko 1.0
 import im.nheko.EmojiModel 1.0
 
@@ -249,4 +249,23 @@ Item {
         roomid: room ? room.roomId : ""
     }
 
+    Connections {
+        function onOpenReadReceiptsDialog(rr) {
+            var dialog = readReceiptsDialog.createObject(timelineRoot, {
+                "readReceipts": rr,
+                "room": room
+            });
+            dialog.show();
+        }
+
+        function onShowRawMessageDialog(rawMessage) {
+            var dialog = rawMessageDialog.createObject(timelineRoot, {
+                "rawMessage": rawMessage
+            });
+            dialog.show();
+        }
+
+        target: room
+    }
+
 }
diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml
index d138060b..767d2317 100644
--- a/resources/qml/UserProfile.qml
+++ b/resources/qml/UserProfile.qml
@@ -4,19 +4,20 @@
 
 import "./device-verification"
 import "./ui"
-import QtQuick 2.9
-import QtQuick.Controls 2.3
+import QtQuick 2.15
+import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
-import QtQuick.Window 2.3
+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
 
-    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
-    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
     height: 650
     width: 420
     minimumHeight: 420
@@ -24,7 +25,8 @@ ApplicationWindow {
     color: Nheko.colors.window
     title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
     modality: Qt.NonModal
-    flags: Qt.Dialog
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(userProfileDialog)
 
     Shortcut {
         sequence: StandardKey.Cancel
diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml
new file mode 100644
index 00000000..36c26a97
--- /dev/null
+++ b/resources/qml/components/AvatarListTile.qml
@@ -0,0 +1,133 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import im.nheko 1.0
+
+Rectangle {
+    id: tile
+
+    property color background: 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
+    property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
+    required property string avatarUrl
+    required property string title
+    required property string subtitle
+    required property int index
+    required property int selectedIndex
+    property bool crop: true
+
+    color: background
+    height: avatarSize + 2 * Nheko.paddingMedium
+    width: ListView.view.width
+    state: "normal"
+    states: [
+        State {
+            name: "highlight"
+            when: hovered.hovered && !(index == selectedIndex)
+
+            PropertyChanges {
+                target: tile
+                background: Nheko.colors.dark
+                importantText: Nheko.colors.brightText
+                unimportantText: Nheko.colors.brightText
+                bubbleBackground: Nheko.colors.highlight
+                bubbleText: Nheko.colors.highlightedText
+            }
+
+        },
+        State {
+            name: "selected"
+            when: index == selectedIndex
+
+            PropertyChanges {
+                target: tile
+                background: Nheko.colors.highlight
+                importantText: Nheko.colors.highlightedText
+                unimportantText: Nheko.colors.highlightedText
+                bubbleBackground: Nheko.colors.highlightedText
+                bubbleText: Nheko.colors.highlight
+            }
+
+        }
+    ]
+
+    HoverHandler {
+        id: hovered
+    }
+
+    RowLayout {
+        spacing: Nheko.paddingMedium
+        anchors.fill: parent
+        anchors.margins: Nheko.paddingMedium
+
+        Avatar {
+            id: avatar
+
+            enabled: false
+            Layout.alignment: Qt.AlignVCenter
+            height: avatarSize
+            width: avatarSize
+            url: tile.avatarUrl.replace("mxc://", "image://MxcImage/")
+            displayName: title
+            crop: tile.crop
+        }
+
+        ColumnLayout {
+            id: textContent
+
+            Layout.alignment: Qt.AlignLeft
+            Layout.fillWidth: true
+            Layout.minimumWidth: 100
+            width: parent.width - avatar.width
+            Layout.preferredWidth: parent.width - avatar.width
+            spacing: Nheko.paddingSmall
+
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 0
+
+                ElidedLabel {
+                    Layout.alignment: Qt.AlignBottom
+                    color: tile.importantText
+                    elideWidth: textContent.width - Nheko.paddingMedium
+                    fullText: title
+                    textFormat: Text.PlainText
+                }
+
+                Item {
+                    Layout.fillWidth: true
+                }
+
+            }
+
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 0
+
+                ElidedLabel {
+                    color: tile.unimportantText
+                    font.pixelSize: fontMetrics.font.pixelSize * 0.9
+                    elideWidth: textContent.width - Nheko.paddingSmall
+                    fullText: subtitle
+                    textFormat: Text.PlainText
+                }
+
+                Item {
+                    Layout.fillWidth: true
+                }
+
+            }
+
+        }
+
+    }
+
+}
diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml
new file mode 100644
index 00000000..cd00a9d4
--- /dev/null
+++ b/resources/qml/delegates/Encrypted.qml
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+ColumnLayout {
+    id: r
+
+    required property int encryptionError
+    required property string eventId
+
+    width: parent ? parent.width : undefined
+
+    MatrixText {
+        text: {
+            switch (encryptionError) {
+            case Olm.MissingSession:
+                return qsTr("There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.");
+            case Olm.MissingSessionIndex:
+                return qsTr("This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message.");
+            case Olm.DbError:
+                return qsTr("There was an internal error reading the decryption key from the database.");
+            case Olm.DecryptionFailed:
+                return qsTr("There was an error decrypting this message.");
+            case Olm.ParsingFailed:
+                return qsTr("The message couldn't be parsed.");
+            case Olm.ReplayAttack:
+                return qsTr("The encryption key was reused! Someone is possibly trying to insert false messages into this chat!");
+            default:
+                return qsTr("Unknown decryption error");
+            }
+        }
+        color: Nheko.colors.buttonText
+        width: r ? r.width : undefined
+    }
+
+    Button {
+        palette: Nheko.colors
+        visible: encryptionError == Olm.MissingSession || encryptionError == Olm.MissingSessionIndex
+        text: qsTr("Request key")
+        onClicked: room.requestKeyForEvent(eventId)
+    }
+
+}
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index a98c2a8b..a8bdf183 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -29,6 +29,7 @@ Item {
     required property string roomTopic
     required property string roomName
     required property string callType
+    required property int encryptionError
     required property int relatedEventCacheBuster
 
     height: chooser.childrenRect.height
@@ -190,6 +191,16 @@ Item {
         }
 
         DelegateChoice {
+            roleValue: MtxEvent.Encrypted
+
+            Encrypted {
+                encryptionError: d.encryptionError
+                eventId: d.eventId
+            }
+
+        }
+
+        DelegateChoice {
             roleValue: MtxEvent.Name
 
             NoticeMessage {
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 75e3d617..8bbce10e 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -5,7 +5,7 @@
 import QtQuick 2.12
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
-import QtQuick.Window 2.2
+import QtQuick.Window 2.13
 import im.nheko 1.0
 
 Item {
@@ -30,6 +30,7 @@ Item {
     property string roomTopic
     property string roomName
     property string callType
+    property int encryptionError
     property int relatedEventCacheBuster
 
     width: parent.width
@@ -97,6 +98,7 @@ Item {
             roomName: r.roomName
             callType: r.callType
             relatedEventCacheBuster: r.relatedEventCacheBuster
+            encryptionError: r.encryptionError
             enabled: false
             width: parent.width
             isReply: true
diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
index e2c66c5a..8e0271d6 100644
--- a/resources/qml/device-verification/DeviceVerification.qml
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -4,7 +4,7 @@
 
 import QtQuick 2.10
 import QtQuick.Controls 2.3
-import QtQuick.Window 2.10
+import QtQuick.Window 2.13
 import im.nheko 1.0
 
 ApplicationWindow {
@@ -14,13 +14,12 @@ ApplicationWindow {
 
     onClosing: TimelineManager.removeVerificationFlow(flow)
     title: stack.currentItem.title
-    flags: Qt.Dialog
     modality: Qt.NonModal
     palette: Nheko.colors
     height: stack.implicitHeight
     width: stack.implicitWidth
-    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
-    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(dialog)
 
     StackView {
         id: stack
diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml
new file mode 100644
index 00000000..b839c9e3
--- /dev/null
+++ b/resources/qml/dialogs/ImagePackEditorDialog.qml
@@ -0,0 +1,301 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import "../components"
+import Qt.labs.platform 1.1
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import im.nheko 1.0
+
+ApplicationWindow {
+    //Component.onCompleted: Nheko.reparent(win)
+
+    id: win
+
+    property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
+    property SingleImagePackModel imagePack
+    property int currentImageIndex: -1
+    readonly property int stickerDim: 128
+    readonly property int stickerDimPad: 128 + Nheko.paddingSmall
+
+    title: qsTr("Editing image pack")
+    height: 600
+    width: 600
+    palette: Nheko.colors
+    color: Nheko.colors.base
+    modality: Qt.WindowModal
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+
+    AdaptiveLayout {
+        id: adaptiveView
+
+        anchors.fill: parent
+        singlePageMode: false
+        pageIndex: 0
+
+        AdaptiveLayoutElement {
+            id: packlistC
+
+            visible: Settings.groupView
+            minimumWidth: 200
+            collapsedWidth: 200
+            preferredWidth: 300
+            maximumWidth: 300
+            clip: true
+
+            ListView {
+                //required property bool isEmote
+                //required property bool isSticker
+
+                model: imagePack
+
+                ScrollHelper {
+                    flickable: parent
+                    anchors.fill: parent
+                    enabled: !Settings.mobileMode
+                }
+
+                header: AvatarListTile {
+                    title: imagePack.packname
+                    avatarUrl: imagePack.avatarUrl
+                    subtitle: imagePack.statekey
+                    index: -1
+                    selectedIndex: currentImageIndex
+
+                    TapHandler {
+                        onSingleTapped: currentImageIndex = -1
+                    }
+
+                    Rectangle {
+                        anchors.left: parent.left
+                        anchors.verticalCenter: parent.verticalCenter
+                        height: parent.height - Nheko.paddingSmall * 2
+                        width: 3
+                        color: Nheko.colors.highlight
+                    }
+
+                }
+
+                footer: Button {
+                    palette: Nheko.colors
+                    onClicked: addFilesDialog.open()
+                    width: ListView.view.width
+                    text: qsTr("Add images")
+
+                    FileDialog {
+                        id: addFilesDialog
+
+                        folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
+                        fileMode: FileDialog.OpenFiles
+                        nameFilters: [qsTr("Stickers (*.png *.webp)")]
+                        onAccepted: imagePack.addStickers(files)
+                    }
+
+                }
+
+                delegate: AvatarListTile {
+                    id: packItem
+
+                    property color background: 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
+                    required property string shortCode
+                    required property string url
+                    required property string body
+
+                    title: shortCode
+                    subtitle: body
+                    avatarUrl: url
+                    selectedIndex: currentImageIndex
+                    crop: false
+
+                    TapHandler {
+                        onSingleTapped: currentImageIndex = index
+                    }
+
+                }
+
+            }
+
+        }
+
+        AdaptiveLayoutElement {
+            id: packinfoC
+
+            Rectangle {
+                color: Nheko.colors.window
+
+                GridLayout {
+                    anchors.fill: parent
+                    anchors.margins: Nheko.paddingMedium
+                    visible: currentImageIndex == -1
+                    enabled: visible
+                    columns: 2
+                    rowSpacing: Nheko.paddingLarge
+
+                    Avatar {
+                        Layout.columnSpan: 2
+                        url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/")
+                        displayName: imagePack.packname
+                        height: 130
+                        width: 130
+                        crop: false
+                        Layout.alignment: Qt.AlignHCenter
+                    }
+
+                    MatrixText {
+                        visible: imagePack.roomid
+                        text: qsTr("State key")
+                    }
+
+                    MatrixTextField {
+                        visible: imagePack.roomid
+                        Layout.fillWidth: true
+                        text: imagePack.statekey
+                        onTextEdited: imagePack.statekey = text
+                    }
+
+                    MatrixText {
+                        text: qsTr("Packname")
+                    }
+
+                    MatrixTextField {
+                        Layout.fillWidth: true
+                        text: imagePack.packname
+                        onTextEdited: imagePack.packname = text
+                    }
+
+                    MatrixText {
+                        text: qsTr("Attrbution")
+                    }
+
+                    MatrixTextField {
+                        Layout.fillWidth: true
+                        text: imagePack.attribution
+                        onTextEdited: imagePack.attribution = text
+                    }
+
+                    MatrixText {
+                        text: qsTr("Use as Emoji")
+                    }
+
+                    ToggleButton {
+                        checked: imagePack.isEmotePack
+                        onClicked: imagePack.isEmotePack = checked
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    MatrixText {
+                        text: qsTr("Use as Sticker")
+                    }
+
+                    ToggleButton {
+                        checked: imagePack.isStickerPack
+                        onClicked: imagePack.isStickerPack = checked
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    Item {
+                        Layout.columnSpan: 2
+                        Layout.fillHeight: true
+                    }
+
+                }
+
+                GridLayout {
+                    anchors.fill: parent
+                    anchors.margins: Nheko.paddingMedium
+                    visible: currentImageIndex >= 0
+                    enabled: visible
+                    columns: 2
+                    rowSpacing: Nheko.paddingLarge
+
+                    Avatar {
+                        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)
+                        height: 130
+                        width: 130
+                        crop: false
+                        Layout.alignment: Qt.AlignHCenter
+                    }
+
+                    MatrixText {
+                        text: qsTr("Shortcode")
+                    }
+
+                    MatrixTextField {
+                        Layout.fillWidth: true
+                        text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
+                        onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode)
+                    }
+
+                    MatrixText {
+                        text: qsTr("Body")
+                    }
+
+                    MatrixTextField {
+                        Layout.fillWidth: true
+                        text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body)
+                        onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body)
+                    }
+
+                    MatrixText {
+                        text: qsTr("Use as Emoji")
+                    }
+
+                    ToggleButton {
+                        checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote)
+                        onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsEmote)
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    MatrixText {
+                        text: qsTr("Use as Sticker")
+                    }
+
+                    ToggleButton {
+                        checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker)
+                        onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsSticker)
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    Item {
+                        Layout.columnSpan: 2
+                        Layout.fillHeight: true
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        id: buttons
+
+        Button {
+            text: qsTr("Cancel")
+            DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole
+            onClicked: win.close()
+        }
+
+        Button {
+            text: qsTr("Save")
+            DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole
+            onClicked: {
+                imagePack.save();
+                win.close();
+            }
+        }
+
+    }
+
+}
diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml
index c4b4a885..5181619c 100644
--- a/resources/qml/dialogs/ImagePackSettingsDialog.qml
+++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml
@@ -20,14 +20,21 @@ ApplicationWindow {
     readonly property int stickerDimPad: 128 + Nheko.paddingSmall
 
     title: qsTr("Image pack settings")
-    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
-    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
-    height: 400
-    width: 600
+    height: 600
+    width: 800
     palette: Nheko.colors
     color: Nheko.colors.base
     modality: Qt.NonModal
-    flags: Qt.Dialog
+    flags: Qt.Dialog | Qt.WindowCloseButtonHint
+    Component.onCompleted: Nheko.reparent(win)
+
+    Component {
+        id: packEditor
+
+        ImagePackEditorDialog {
+        }
+
+    }
 
     AdaptiveLayout {
         id: adaptiveView
@@ -55,7 +62,35 @@ ApplicationWindow {
                     enabled: !Settings.mobileMode
                 }
 
-                delegate: Rectangle {
+                footer: ColumnLayout {
+                    Button {
+                        palette: Nheko.colors
+                        onClicked: {
+                            var dialog = packEditor.createObject(timelineRoot, {
+                                "imagePack": packlist.newPack(false)
+                            });
+                            dialog.show();
+                        }
+                        width: packlist.width
+                        visible: !packlist.containsAccountPack
+                        text: qsTr("Create account pack")
+                    }
+
+                    Button {
+                        palette: Nheko.colors
+                        onClicked: {
+                            var dialog = packEditor.createObject(timelineRoot, {
+                                "imagePack": packlist.newPack(true)
+                            });
+                            dialog.show();
+                        }
+                        width: packlist.width
+                        text: qsTr("New room pack")
+                    }
+
+                }
+
+                delegate: AvatarListTile {
                     id: packItem
 
                     property color background: Nheko.colors.window
@@ -64,131 +99,24 @@ ApplicationWindow {
                     property color bubbleBackground: Nheko.colors.highlight
                     property color bubbleText: Nheko.colors.highlightedText
                     required property string displayName
-                    required property string avatarUrl
                     required property bool fromAccountData
                     required property bool fromCurrentRoom
-                    required property int index
-
-                    color: background
-                    height: avatarSize + 2 * Nheko.paddingMedium
-                    width: ListView.view.width
-                    state: "normal"
-                    states: [
-                        State {
-                            name: "highlight"
-                            when: hovered.hovered && !(index == currentPackIndex)
-
-                            PropertyChanges {
-                                target: packItem
-                                background: Nheko.colors.dark
-                                importantText: Nheko.colors.brightText
-                                unimportantText: Nheko.colors.brightText
-                                bubbleBackground: Nheko.colors.highlight
-                                bubbleText: Nheko.colors.highlightedText
-                            }
-
-                        },
-                        State {
-                            name: "selected"
-                            when: index == currentPackIndex
-
-                            PropertyChanges {
-                                target: packItem
-                                background: Nheko.colors.highlight
-                                importantText: Nheko.colors.highlightedText
-                                unimportantText: Nheko.colors.highlightedText
-                                bubbleBackground: Nheko.colors.highlightedText
-                                bubbleText: Nheko.colors.highlight
-                            }
 
-                        }
-                    ]
+                    title: displayName
+                    subtitle: {
+                        if (fromAccountData)
+                            return qsTr("Private pack");
+                        else if (fromCurrentRoom)
+                            return qsTr("Pack from this room");
+                        else
+                            return qsTr("Globally enabled pack");
+                    }
+                    selectedIndex: currentPackIndex
 
                     TapHandler {
-                        margin: -Nheko.paddingSmall
                         onSingleTapped: currentPackIndex = index
                     }
 
-                    HoverHandler {
-                        id: hovered
-                    }
-
-                    RowLayout {
-                        spacing: Nheko.paddingMedium
-                        anchors.fill: parent
-                        anchors.margins: Nheko.paddingMedium
-
-                        Avatar {
-                            // In the future we could show an online indicator by setting the userid for the avatar
-                            //userid: Nheko.currentUser.userid
-
-                            id: avatar
-
-                            enabled: false
-                            Layout.alignment: Qt.AlignVCenter
-                            height: avatarSize
-                            width: avatarSize
-                            url: avatarUrl.replace("mxc://", "image://MxcImage/")
-                            displayName: packItem.displayName
-                        }
-
-                        ColumnLayout {
-                            id: textContent
-
-                            Layout.alignment: Qt.AlignLeft
-                            Layout.fillWidth: true
-                            Layout.minimumWidth: 100
-                            width: parent.width - avatar.width
-                            Layout.preferredWidth: parent.width - avatar.width
-                            spacing: Nheko.paddingSmall
-
-                            RowLayout {
-                                Layout.fillWidth: true
-                                spacing: 0
-
-                                ElidedLabel {
-                                    Layout.alignment: Qt.AlignBottom
-                                    color: packItem.importantText
-                                    elideWidth: textContent.width - Nheko.paddingMedium
-                                    fullText: displayName
-                                    textFormat: Text.PlainText
-                                }
-
-                                Item {
-                                    Layout.fillWidth: true
-                                }
-
-                            }
-
-                            RowLayout {
-                                Layout.fillWidth: true
-                                spacing: 0
-
-                                ElidedLabel {
-                                    color: packItem.unimportantText
-                                    font.pixelSize: fontMetrics.font.pixelSize * 0.9
-                                    elideWidth: textContent.width - Nheko.paddingSmall
-                                    fullText: {
-                                        if (fromAccountData)
-                                            return qsTr("Private pack");
-                                        else if (fromCurrentRoom)
-                                            return qsTr("Pack from this room");
-                                        else
-                                            return qsTr("Globally enabled pack");
-                                    }
-                                    textFormat: Text.PlainText
-                                }
-
-                                Item {
-                                    Layout.fillWidth: true
-                                }
-
-                            }
-
-                        }
-
-                    }
-
                 }
 
             }
@@ -205,6 +133,7 @@ ApplicationWindow {
                     id: packinfo
 
                     property string packName: currentPack ? currentPack.packname : ""
+                    property string attribution: currentPack ? currentPack.attribution : ""
                     property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
 
                     anchors.fill: parent
@@ -222,8 +151,18 @@ ApplicationWindow {
 
                     MatrixText {
                         text: packinfo.packName
-                        font.pixelSize: 24
+                        font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1)
+                        horizontalAlignment: TextEdit.AlignHCenter
                         Layout.alignment: Qt.AlignHCenter
+                        Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2
+                    }
+
+                    MatrixText {
+                        text: packinfo.attribution
+                        wrapMode: TextEdit.Wrap
+                        horizontalAlignment: TextEdit.AlignHCenter
+                        Layout.alignment: Qt.AlignHCenter
+                        Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2
                     }
 
                     GridLayout {
@@ -245,6 +184,18 @@ ApplicationWindow {
 
                     }
 
+                    Button {
+                        Layout.alignment: Qt.AlignHCenter
+                        text: qsTr("Edit")
+                        enabled: currentPack.canEdit
+                        onClicked: {
+                            var dialog = packEditor.createObject(timelineRoot, {
+                                "imagePack": currentPack
+                            });
+                            dialog.show();
+                        }
+                    }
+
                     GridView {
                         Layout.fillHeight: true
                         Layout.fillWidth: true
@@ -267,7 +218,7 @@ ApplicationWindow {
                             width: stickerDim
                             height: stickerDim
                             hoverEnabled: true
-                            ToolTip.text: ":" + model.shortcode + ": - " + model.body
+                            ToolTip.text: ":" + model.shortCode + ": - " + model.body
                             ToolTip.visible: hovered
 
                             contentItem: Image {
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
index 134b78a3..e0f17851 100644
--- a/resources/qml/dialogs/InputDialog.qml
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -16,6 +16,7 @@ ApplicationWindow {
 
     modality: Qt.NonModal
     flags: Qt.Dialog
+    Component.onCompleted: Nheko.reparent(inputDialog)
     width: 350
     height: fontMetrics.lineSpacing * 7
 
diff --git a/resources/res.qrc b/resources/res.qrc
index 407a0a98..3e417d4c 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -112,7 +112,6 @@
     </qresource>
     <qresource prefix="/">
         <file>qtquickcontrols2.conf</file>
-
         <file>qml/Root.qml</file>
         <file>qml/ChatPage.qml</file>
         <file>qml/CommunitiesList.qml</file>
@@ -144,15 +143,21 @@
         <file>qml/emoji/StickerPicker.qml</file>
         <file>qml/UserProfile.qml</file>
         <file>qml/RoomDirectory.qml</file>
-	<file>qml/delegates/MessageDelegate.qml</file>
+	      <file>qml/delegates/MessageDelegate.qml</file>
         <file>qml/delegates/TextMessage.qml</file>
         <file>qml/delegates/NoticeMessage.qml</file>
         <file>qml/delegates/ImageMessage.qml</file>
         <file>qml/delegates/PlayableMediaMessage.qml</file>
+        <file>qml/delegates/MessageDelegate.qml</file>
+        <file>qml/delegates/Encrypted.qml</file>
         <file>qml/delegates/FileMessage.qml</file>
+        <file>qml/delegates/ImageMessage.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>
@@ -162,6 +167,7 @@
         <file>qml/device-verification/Success.qml</file>
         <file>qml/dialogs/InputDialog.qml</file>
         <file>qml/dialogs/ImagePackSettingsDialog.qml</file>
+        <file>qml/dialogs/ImagePackEditorDialog.qml</file>
         <file>qml/ui/Ripple.qml</file>
         <file>qml/ui/Spinner.qml</file>
         <file>qml/ui/animations/BlinkAnimation.qml</file>
@@ -175,9 +181,12 @@
         <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/src/Cache.cpp b/src/Cache.cpp
index 4c24a712..6650334a 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -125,7 +125,7 @@ template<class T>
 bool
 containsStateUpdates(const T &e)
 {
-        return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e);
+        return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
 }
 
 bool
@@ -288,6 +288,9 @@ Cache::setup()
         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);
+
         txn.commit();
 
         databaseReady_ = true;
@@ -298,8 +301,7 @@ Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
 {
         nhlog::db()->info("mark room {} as encrypted", room_id);
 
-        auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
-        db.put(txn, room_id, "0");
+        encryptedRooms_.put(txn, room_id, "0");
 }
 
 bool
@@ -308,8 +310,7 @@ Cache::isRoomEncrypted(const std::string &room_id)
         std::string_view unused;
 
         auto txn = ro_txn(env_);
-        auto db  = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
-        auto res = db.get(txn, room_id, unused);
+        auto res = encryptedRooms_.get(txn, room_id, unused);
 
         return res;
 }
@@ -3400,7 +3401,7 @@ Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
                         info.pack.pack   = pack.pack;
 
                         for (const auto &img : pack.images) {
-                                if (img.second.overrides_usage() &&
+                                if (stickers.has_value() && img.second.overrides_usage() &&
                                     (stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
                                         continue;
 
@@ -3541,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id)
 }
 
 std::map<std::string, std::optional<UserKeyCache>>
-Cache::getMembersWithKeys(const std::string &room_id)
+Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
 {
         std::string_view keys;
 
@@ -3558,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id)
                         auto res = keysDb.get(txn, user_id, keys);
 
                         if (res) {
-                                members[std::string(user_id)] =
-                                  json::parse(keys).get<UserKeyCache>();
+                                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 {
-                                members[std::string(user_id)] = {};
+                                if (!verified_only)
+                                        members[std::string(user_id)] = {};
                         }
                 }
                 cursor.close();
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 89c88925..30c365a6 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -48,7 +48,8 @@ public:
         // 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);
+          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,
@@ -290,15 +291,9 @@ public:
         std::optional<std::string> secret(const std::string name);
 
         template<class T>
-        static constexpr bool isStateEvent(const mtx::events::StateEvent<T> &)
-        {
-                return true;
-        }
-        template<class T>
-        static constexpr bool isStateEvent(const mtx::events::Event<T> &)
-        {
-                return false;
-        }
+        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)
         {
@@ -415,11 +410,27 @@ private:
                 }
 
                 std::visit(
-                  [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
-                          if constexpr (isStateEvent(e)) {
+                  [&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.type != EventType::Unsupported) {
                                           if (e.state_key.empty())
                                                   statesdb.put(
                                                     txn, to_string(e.type), json(e).dump());
@@ -689,6 +700,8 @@ private:
         lmdb::dbi outboundMegolmSessionDb_;
         lmdb::dbi megolmSessionDataDb_;
 
+        lmdb::dbi encryptedRooms_;
+
         QString localUserId_;
         QString cacheDirectory_;
 
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index a76756ae..42e3bc7b 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -31,7 +31,6 @@
 
 #include "notifications/Manager.h"
 
-#include "dialogs/ReadReceipts.h"
 #include "timeline/TimelineViewManager.h"
 
 #include "blurhash.hpp"
diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp
index 89f1f68e..6392de22 100644
--- a/src/ImagePackListModel.cpp
+++ b/src/ImagePackListModel.cpp
@@ -74,3 +74,21 @@ ImagePackListModel::packAt(int row)
         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);
+}
+
+bool
+ImagePackListModel::containsAccountPack() const
+{
+        for (const auto &p : packs)
+                if (p->roomid().isEmpty())
+                        return true;
+        return false;
+}
diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h
index 0a044690..2aa5abb2 100644
--- a/src/ImagePackListModel.h
+++ b/src/ImagePackListModel.h
@@ -12,6 +12,7 @@ class SingleImagePackModel;
 class ImagePackListModel : public QAbstractListModel
 {
         Q_OBJECT
+        Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
 public:
         enum Roles
         {
@@ -29,6 +30,9 @@ public:
         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;
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index c0486d01..8bc90f29 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -36,7 +36,6 @@
 #include "dialogs/JoinRoom.h"
 #include "dialogs/LeaveRoom.h"
 #include "dialogs/Logout.h"
-#include "dialogs/ReadReceipts.h"
 
 MainWindow *MainWindow::instance_ = nullptr;
 
@@ -398,27 +397,6 @@ MainWindow::openLogoutDialog()
         showDialog(dialog);
 }
 
-void
-MainWindow::openReadReceiptsDialog(const QString &event_id)
-{
-        auto dialog = new dialogs::ReadReceipts(this);
-
-        const auto room_id = chat_page_->currentRoom();
-
-        try {
-                dialog->addUsers(cache::readReceipts(event_id, room_id));
-        } catch (const lmdb::error &) {
-                nhlog::db()->warn("failed to retrieve read receipts for {} {}",
-                                  event_id.toStdString(),
-                                  chat_page_->currentRoom().toStdString());
-                dialog->deleteLater();
-
-                return;
-        }
-
-        showDialog(dialog);
-}
-
 bool
 MainWindow::hasActiveDialogs() const
 {
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 6d62545c..d423af9f 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -65,7 +65,6 @@ public:
           std::function<void(const mtx::requests::CreateRoom &request)> callback);
         void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
         void openLogoutDialog();
-        void openReadReceiptsDialog(const QString &event_id);
 
         void hideOverlay();
         void showSolidOverlayModal(QWidget *content,
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
index 0ef3b696..196647fe 100644
--- a/src/MemberList.cpp
+++ b/src/MemberList.cpp
@@ -2,16 +2,6 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-#include <QAbstractSlider>
-#include <QLabel>
-#include <QListWidgetItem>
-#include <QPainter>
-#include <QPushButton>
-#include <QScrollBar>
-#include <QShortcut>
-#include <QStyleOption>
-#include <QVBoxLayout>
-
 #include "MemberList.h"
 
 #include "Cache.h"
@@ -20,7 +10,6 @@
 #include "Logging.h"
 #include "Utils.h"
 #include "timeline/TimelineViewManager.h"
-#include "ui/Avatar.h"
 
 MemberList::MemberList(const QString &room_id, QObject *parent)
   : QAbstractListModel{parent}
diff --git a/src/MemberList.h b/src/MemberList.h
index 9932f6a4..e6522694 100644
--- a/src/MemberList.h
+++ b/src/MemberList.h
@@ -4,9 +4,10 @@
 
 #pragma once
 
-#include "CacheStructs.h"
 #include <QAbstractListModel>
 
+#include "CacheStructs.h"
+
 class MemberList : public QAbstractListModel
 {
         Q_OBJECT
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index ab0f8152..b8648269 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -22,7 +22,14 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
 QQuickImageResponse *
 MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
 {
-        MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
+        auto id_  = id;
+        bool crop = true;
+        if (id.endsWith("?scale")) {
+                crop = false;
+                id_.remove("?scale");
+        }
+
+        MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
         pool.start(response);
         return response;
 }
@@ -36,20 +43,24 @@ void
 MxcImageResponse::run()
 {
         MxcImageProvider::download(
-          m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
+          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);
 }
 
 void
 MxcImageProvider::download(const QString &id,
                            const QSize &requestedSize,
-                           std::function<void(QString, QSize, QImage, QString)> then)
+                           std::function<void(QString, QSize, QImage, QString)> then,
+                           bool crop)
 {
         std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
         auto temp = infos.find("mxc://" + id);
@@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id,
 
         if (requestedSize.isValid() && !encryptionInfo) {
                 QString fileName =
-                  QString("%1_%2x%3_crop")
+                  QString("%1_%2x%3_%4")
                     .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
                                                                 QByteArray::OmitTrailingEquals)))
                     .arg(requestedSize.width())
-                    .arg(requestedSize.height());
+                    .arg(requestedSize.height())
+                    .arg(crop ? "crop" : "scale");
                 QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
                                      "/media_cache",
                                    fileName);
@@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id,
                 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";
+                opts.method  = crop ? "crop" : "scale";
                 http::client()->get_thumbnail(
                   opts,
                   [fileInfo, requestedSize, then, id](const std::string &res,
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
index 7b960836..61d82852 100644
--- a/src/MxcImageProvider.h
+++ b/src/MxcImageProvider.h
@@ -19,9 +19,10 @@ class MxcImageResponse
   , public QRunnable
 {
 public:
-        MxcImageResponse(const QString &id, const QSize &requestedSize)
+        MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
           : m_id(id)
           , m_requestedSize(requestedSize)
+          , m_crop(crop)
         {
                 setAutoDelete(false);
         }
@@ -37,6 +38,7 @@ public:
         QString m_id, m_error;
         QSize m_requestedSize;
         QImage m_image;
+        bool m_crop;
 };
 
 class MxcImageProvider
@@ -51,7 +53,8 @@ public slots:
         static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
         static void download(const QString &id,
                              const QSize &requestedSize,
-                             std::function<void(QString, QSize, QImage, QString)> then);
+                             std::function<void(QString, QSize, QImage, QString)> then,
+                             bool crop = true);
 
 private:
         QThreadPool pool;
diff --git a/src/Olm.cpp b/src/Olm.cpp
index d20bf9a4..293b12de 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -286,11 +286,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
 
                         bool from_their_device = false;
                         for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
-                                if (key.keys.at("curve25519:" + device_id) == msg.sender_key) {
-                                        if (key.keys.at("ed25519:" + device_id) == sender_ed25519) {
-                                                from_their_device = true;
-                                                break;
-                                        }
+                                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) {
@@ -518,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
 
         auto own_user_id = http::client()->user_id().to_string();
 
-        auto members = cache::client()->getMembersWithKeys(room_id);
+        auto members = cache::client()->getMembersWithKeys(
+          room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers());
 
         std::map<std::string, std::vector<std::string>> sendSessionTo;
         mtx::crypto::OutboundGroupSessionPtr session = nullptr;
@@ -1062,7 +1069,7 @@ decryptEvent(const MegolmSessionIndex &index,
                 mtx::events::collections::TimelineEvent te;
                 mtx::events::collections::from_json(body, te);
 
-                return {std::nullopt, std::nullopt, std::move(te.data)};
+                return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)};
         } catch (std::exception &e) {
                 return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
         }
@@ -1138,9 +1145,23 @@ 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) {
-                                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);
+                                static QMap<QPair<std::string, std::string>, qint64> rateLimit;
+                                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;
                         }
 
diff --git a/src/Olm.h b/src/Olm.h
index a18cbbfb..ac1a1617 100644
--- a/src/Olm.h
+++ b/src/Olm.h
@@ -14,9 +14,11 @@
 constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
 
 namespace olm {
+Q_NAMESPACE
 
-enum class DecryptionErrorCode
+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,
@@ -25,14 +27,13 @@ enum class DecryptionErrorCode
         DecryptionFailed,    // libolm error
         ParsingFailed,       // Failed to parse the actual event
         ReplayAttack,        // Megolm index reused
-        UnknownFingerprint,  // Unknown device Fingerprint
 };
+Q_ENUM_NS(DecryptionErrorCode)
 
 struct DecryptionResult
 {
-        std::optional<DecryptionErrorCode> error;
+        DecryptionErrorCode error;
         std::optional<std::string> error_message;
-
         std::optional<mtx::events::collections::TimelineEvents> event;
 };
 
diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp
new file mode 100644
index 00000000..25262c59
--- /dev/null
+++ b/src/ReadReceiptsModel.cpp
@@ -0,0 +1,131 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "ReadReceiptsModel.h"
+
+#include <QLocale>
+
+#include "Cache.h"
+#include "Cache_p.h"
+#include "Logging.h"
+#include "Utils.h"
+
+ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject *parent)
+  : QAbstractListModel{parent}
+  , 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;
+        }
+
+        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;
+        }
+}
+
+QHash<int, QByteArray>
+ReadReceiptsModel::roleNames() const
+{
+        // 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 {};
+        }
+}
+
+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);
+
+                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));
+
+        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);
+}
diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h
new file mode 100644
index 00000000..3b45716c
--- /dev/null
+++ b/src/ReadReceiptsModel.h
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef READRECEIPTSMODEL_H
+#define READRECEIPTSMODEL_H
+
+#include <QAbstractListModel>
+#include <QDateTime>
+#include <QObject>
+#include <QSortFilterProxyModel>
+#include <QString>
+
+class ReadReceiptsModel : public QAbstractListModel
+{
+        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;
+
+public slots:
+        void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
+        void update();
+
+private:
+        QString dateFormat(const QDateTime &then) const;
+
+        QString event_id_;
+        QString room_id_;
+        QVector<QPair<QString, QDateTime>> readReceipts_;
+};
+
+class ReadReceiptsProxy : public QSortFilterProxyModel
+{
+        Q_OBJECT
+
+        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);
+
+        QString eventId() const { return event_id_; }
+        QString roomId() const { return room_id_; }
+
+private:
+        QString event_id_;
+        QString room_id_;
+
+        ReadReceiptsModel model_;
+};
+
+#endif // READRECEIPTSMODEL_H
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index 1588d07d..bae24df0 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -12,6 +12,7 @@
 
 #include <mtx/responses/register.hpp>
 #include <mtx/responses/well-known.hpp>
+#include <mtxclient/http/client.hpp>
 
 #include "Config.h"
 #include "Logging.h"
@@ -93,6 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent)
 
         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."));
@@ -145,178 +147,39 @@ RegisterPage::RegisterPage(QWidget *parent)
         top_layout_->addLayout(button_layout_);
         top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
         top_layout_->addStretch(1);
-
-        connect(
-          this,
-          &RegisterPage::versionErrorCb,
-          this,
-          [this](const QString &msg) {
-                  error_server_label_->show();
-                  server_input_->setValid(false);
-                  showError(error_server_label_, msg);
-          },
-          Qt::QueuedConnection);
+        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::checkFields);
+        connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
         connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
+        connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
         connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(
-          password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields);
+        connect(password_confirmation_,
+                &TextField::editingFinished,
+                this,
+                &RegisterPage::checkPasswordConfirmation);
         connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-        connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
-        connect(this, &RegisterPage::registerErrorCb, this, [this](const QString &msg) {
-                showError(msg);
-        });
-        connect(
-          this,
-          &RegisterPage::registrationFlow,
-          this,
-          [this](const std::string &user,
-                 const std::string &pass,
-                 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())
-                          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());
-
-                  if (flows.empty()) {
-                          nhlog::net()->error("No available registration flows!");
-                          emit registerErrorCb(tr("No supported registration flows!"));
-                          return;
-                  }
-
-                  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, user, pass, session, captchaDialog]() {
-                                          captchaDialog->close();
-                                          captchaDialog->deleteLater();
-
-                                          emit registerAuth(
-                                            user,
-                                            pass,
-                                            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) {
-                          emit registerAuth(user,
-                                            pass,
-                                            mtx::user_interactive::Auth{
-                                              session, mtx::user_interactive::auth::Dummy{}});
-                  } else {
-                          // use fallback
-                          auto dialog =
-                            new dialogs::FallbackAuth(QString::fromStdString(current_stage),
-                                                      QString::fromStdString(session),
-                                                      this);
-
-                          connect(dialog,
-                                  &dialogs::FallbackAuth::confirmation,
-                                  this,
-                                  [this, user, pass, session, dialog]() {
-                                          dialog->close();
-                                          dialog->deleteLater();
-
-                                          emit registerAuth(
-                                            user,
-                                            pass,
-                                            mtx::user_interactive::Auth{
-                                              session, mtx::user_interactive::auth::Fallback{}});
-                                  });
-                          connect(dialog,
-                                  &dialogs::FallbackAuth::cancel,
-                                  this,
-                                  &RegisterPage::errorOccurred);
-
-                          dialog->show();
-                  }
-          });
+        connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
 
         connect(
           this,
-          &RegisterPage::registerAuth,
+          &RegisterPage::serverError,
           this,
-          [this](const std::string &user,
-                 const std::string &pass,
-                 const mtx::user_interactive::Auth &auth) {
-                  http::client()->registration(
-                    user,
-                    pass,
-                    auth,
-                    [this, user, pass](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);
-                                    http::client()->set_device_id(res.device_id);
-
-                                    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: ({}) "
-                                              "{}",
-                                              static_cast<int>(err->status_code),
-                                              err->matrix_error.error);
-                                            emit registerErrorCb(
-                                              QString::fromStdString(err->matrix_error.error));
-                                            return;
-                                    }
-
-                                    emit registrationFlow(
-                                      user, pass, err->matrix_error.unauthorized);
-                                    return;
-                            }
-
-                            nhlog::net()->warn("failed to register: status_code ({}), "
-                                               "matrix_error: ({}), parser error ({})",
-                                               static_cast<int>(err->status_code),
-                                               err->matrix_error.error,
-                                               err->parse_error);
-
-                            emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
-                    });
-          });
+          [this](const QString &msg) {
+                  server_input_->setValid(false);
+                  showError(error_server_label_, msg);
+          },
+          Qt::QueuedConnection);
 
-        setLayout(top_layout_);
+        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);
 }
 
 void
@@ -345,192 +208,299 @@ RegisterPage::showError(QLabel *label, const QString &msg)
         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->setText("");
                 label->hide();
                 return true;
         } else {
-                label->show();
                 showError(label, msg);
                 return false;
         }
 }
 
 bool
-RegisterPage::checkFields()
+RegisterPage::checkUsername()
 {
-        error_label_->setText("");
-        error_username_label_->setText("");
-        error_password_label_->setText("");
-        error_password_confirmation_label_->setText("");
-        error_server_label_->setText("");
+        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 /."));
+}
 
-        error_username_label_->hide();
-        error_password_label_->hide();
-        error_password_confirmation_label_->hide();
-        error_server_label_->hide();
+bool
+RegisterPage::checkPassword()
+{
+        return checkOneField(
+          error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
+}
 
-        password_confirmation_->setValid(true);
-        server_input_->setValid(true);
-
-        bool all_fields_good = true;
-        if (username_input_->isModified() &&
-            !checkOneField(error_username_label_,
-                           username_input_,
-                           tr("The username must not be empty, and must contain only the "
-                              "characters a-z, 0-9, ., _, =, -, and /."))) {
-                all_fields_good = false;
-        } else if (password_input_->isModified() &&
-                   !checkOneField(error_password_label_,
-                                  password_input_,
-                                  tr("Password is not long enough (min 8 chars)"))) {
-                all_fields_good = false;
-        } else if (password_confirmation_->isModified() &&
-                   password_input_->text() != password_confirmation_->text()) {
-                error_password_confirmation_label_->show();
+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);
-                all_fields_good = false;
-        } else if (server_input_->isModified() &&
-                   (!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) {
-                error_server_label_->show();
-                showError(error_server_label_, tr("Invalid server name"));
-                server_input_->setValid(false);
-                all_fields_good = false;
-        }
-        if (!username_input_->isModified() || !password_input_->isModified() ||
-            !password_confirmation_->isModified() || !server_input_->isModified()) {
-                all_fields_good = false;
+                return false;
         }
-        return all_fields_good;
+}
+
+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"));
 }
 
 void
 RegisterPage::onRegisterButtonClicked()
 {
-        if (!checkFields()) {
-                showError(error_label_,
-                          tr("One or more fields have invalid inputs. Please correct those issues "
-                             "and try again."));
-                return;
-        } else {
-                auto username = username_input_->text().toStdString();
-                auto password = password_input_->text().toStdString();
-                auto server   = server_input_->text().toStdString();
+        if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
+                auto server = server_input_->text().toStdString();
 
                 http::client()->set_server(server);
                 http::client()->verify_certificates(
                   !UserSettings::instance()->disableCertificateValidation());
 
-                http::client()->well_known(
-                  [this, username, password](const mtx::responses::WellKnown &res,
-                                             mtx::http::RequestErr err) {
-                          if (err) {
-                                  if (err->status_code == 404) {
-                                          nhlog::net()->info("Autodiscovery: No .well-known.");
-                                          checkVersionAndRegister(username, password);
-                                          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);
+                // 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();
+        }
+}
+
+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;
                           }
 
-                          nhlog::net()->info("Autodiscovery: Discovered '" +
-                                             res.homeserver.base_url + "'");
-                          http::client()->set_server(res.homeserver.base_url);
-                          checkVersionAndRegister(username, password);
-                  });
+                          if (!err->parse_error.empty()) {
+                                  emit serverError(
+                                    tr("Autodiscovery failed. Received malformed response."));
+                                  nhlog::net()->error(
+                                    "Autodiscovery failed. Received malformed response.");
+                                  return;
+                          }
 
-                emit registering();
-        }
+                          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::checkVersionAndRegister(const std::string &username, const std::string &password)
+RegisterPage::doVersionsCheck()
 {
+        // Make a request to /_matrix/client/versions to check the address
+        // given is a Matrix homeserver.
         http::client()->versions(
-          [this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+          [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."));
+                                  emit serverError(
+                                    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."));
+                                  emit serverError(
+                                    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."));
+                          emit serverError(tr("An unknown error occured. Make sure the "
+                                              "homeserver domain is valid."));
                           return;
                   }
 
-                  http::client()->registration(
-                    username,
-                    password,
-                    [this, username, password](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 flows1: ({}) "
-                                              "{}",
-                                              static_cast<int>(err->status_code),
-                                              err->matrix_error.error);
-                                            emit errorOccurred();
-                                            emit registerErrorCb(
-                                              QString::fromStdString(err->matrix_error.error));
-                                            return;
-                                    }
-
-                                    emit registrationFlow(
-                                      username, password, 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);
-
-                            emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
-                            emit errorOccurred();
-                    });
+                  // Attempt registration without an `auth` dict
+                  emit registration();
           });
 }
 
 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());
+        }
+}
+
+void
+RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth)
+{
+        // 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());
+        }
+}
+
+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);
+
+                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());
+        }
+
+        if (flows.empty()) {
+                nhlog::ui()->error("No available registration flows!");
+                showError(tr("No supported registration flows!"));
+                return;
+        }
+
+        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 {
+                // 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();
+        }
+}
+
+void
 RegisterPage::paintEvent(QPaintEvent *)
 {
         QStyleOption opt;
diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index 0e4a45d0..42ea00cb 100644
--- a/src/RegisterPage.h
+++ b/src/RegisterPage.h
@@ -10,6 +10,7 @@
 #include <memory>
 
 #include <mtx/user_interactive.hpp>
+#include <mtxclient/http/client.hpp>
 
 class FlatButton;
 class RaisedButton;
@@ -33,17 +34,16 @@ signals:
         void errorOccurred();
 
         //! Used to trigger the corresponding slot outside of the main thread.
-        void versionErrorCb(const QString &err);
+        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 registering();
         void registerOk();
-        void registerErrorCb(const QString &msg);
-        void registrationFlow(const std::string &user,
-                              const std::string &pass,
-                              const mtx::user_interactive::Unauthorized &unauthorized);
-        void registerAuth(const std::string &user,
-                          const std::string &pass,
-                          const mtx::user_interactive::Auth &auth);
 
 private slots:
         void onBackButtonClicked();
@@ -51,12 +51,22 @@ private slots:
 
         // function for showing different errors
         void showError(const QString &msg);
+        void showError(QLabel *label, const QString &msg);
 
-private:
         bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
-        bool checkFields();
-        void showError(QLabel *label, const QString &msg);
-        void checkVersionAndRegister(const std::string &username, const std::string &password);
+        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();
+
+private:
         QVBoxLayout *top_layout_;
 
         QHBoxLayout *back_layout_;
@@ -69,6 +79,7 @@ private:
         QLabel *error_password_label_;
         QLabel *error_password_confirmation_label_;
         QLabel *error_server_label_;
+        QLabel *error_registration_token_label_;
 
         FlatButton *back_button_;
         RaisedButton *register_button_;
@@ -81,4 +92,5 @@ private:
         TextField *password_input_;
         TextField *password_confirmation_;
         TextField *server_input_;
+        TextField *registration_token_input_;
 };
diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp
index 6c508da0..7bf55617 100644
--- a/src/SingleImagePackModel.cpp
+++ b/src/SingleImagePackModel.cpp
@@ -4,20 +4,35 @@
 
 #include "SingleImagePackModel.h"
 
+#include <QFile>
+#include <QMimeDatabase>
+
 #include "Cache_p.h"
+#include "ChatPage.h"
+#include "Logging.h"
 #include "MatrixClient.h"
+#include "Utils.h"
+#include "timeline/Permissions.h"
+#include "timeline/TimelineModel.h"
+
+Q_DECLARE_METATYPE(mtx::common::ImageInfo)
 
 SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
   : QAbstractListModel(parent)
   , roomid_(std::move(pack_.source_room))
   , statekey_(std::move(pack_.state_key))
+  , old_statekey_(statekey_)
   , pack(std::move(pack_.pack))
 {
+        [[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
+
         if (!pack.pack)
                 pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
 
         for (const auto &e : pack.images)
                 shortcodes.push_back(e.first);
+
+        connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
 }
 
 int
@@ -62,6 +77,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const
 }
 
 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;
+                }
+                }
+        }
+        return false;
+}
+
+bool
 SingleImagePackModel::isGloballyEnabled() const
 {
         if (auto roomPacks =
@@ -98,3 +180,171 @@ SingleImagePackModel::setGloballyEnabled(bool enabled)
                 // emit this->globallyEnabledChanged();
         });
 }
+
+bool
+SingleImagePackModel::canEdit() const
+{
+        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();
+        }
+}
+
+void
+SingleImagePackModel::setAttribution(QString val)
+{
+        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();
+        }
+}
+
+void
+SingleImagePackModel::setStatekey(QString val)
+{
+        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();
+        }
+}
+
+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();
+        }
+}
+
+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_);
+                  });
+        }
+}
+
+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);
+                  });
+        }
+}
+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()));
+
+        pack.images[filename] = img;
+        shortcodes.push_back(filename);
+
+        endInsertRows();
+}
diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h
index e0c791ba..cd38b3b6 100644
--- a/src/SingleImagePackModel.h
+++ b/src/SingleImagePackModel.h
@@ -5,6 +5,8 @@
 #pragma once
 
 #include <QAbstractListModel>
+#include <QList>
+#include <QUrl>
 
 #include <mtx/events/mscs/image_packs.hpp>
 
@@ -15,14 +17,18 @@ class SingleImagePackModel : public QAbstractListModel
         Q_OBJECT
 
         Q_PROPERTY(QString roomid READ roomid CONSTANT)
-        Q_PROPERTY(QString statekey READ statekey CONSTANT)
-        Q_PROPERTY(QString attribution READ statekey CONSTANT)
-        Q_PROPERTY(QString packname READ packname CONSTANT)
-        Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
-        Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT)
-        Q_PROPERTY(bool isEmotePack READ isEmotePack 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
         {
@@ -32,11 +38,15 @@ public:
                 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_); }
@@ -47,14 +57,36 @@ public:
         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);
+
 signals:
         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);
+
+private slots:
+        void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info);
 
 private:
         std::string roomid_;
-        std::string statekey_;
+        std::string statekey_, old_statekey_;
 
         mtx::events::msc2545::ImagePack pack;
         std::vector<std::string> shortcodes;
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index a062780a..ab6ac492 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -90,13 +90,11 @@ UserSettings::load(std::optional<QString> profile)
         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();
-        shareKeysWithTrustedUsers_ =
-          settings.value("user/automatically_share_keys_with_trusted_users", false).toBool();
-        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());
+        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);
@@ -123,6 +121,12 @@ UserSettings::load(std::optional<QString> profile)
         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();
 
@@ -402,6 +406,17 @@ UserSettings::setUseStunServer(bool useStunServer)
 }
 
 void
+UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys)
+{
+        if (shareKeys == onlyShareKeysWithVerifiedUsers_)
+                return;
+
+        onlyShareKeysWithVerifiedUsers_ = shareKeys;
+        emit onlyShareKeysWithVerifiedUsersChanged(shareKeys);
+        save();
+}
+
+void
 UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
 {
         if (shareKeys == shareKeysWithTrustedUsers_)
@@ -610,8 +625,6 @@ UserSettings::save()
         settings.setValue("decrypt_sidebar", decryptSidebar_);
         settings.setValue("privacy_screen", privacyScreen_);
         settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
-        settings.setValue("automatically_share_keys_with_trusted_users",
-                          shareKeysWithTrustedUsers_);
         settings.setValue("mobile_mode", mobileMode_);
         settings.setValue("font_size", baseFontSize_);
         settings.setValue("typing_notifications", typingNotifications_);
@@ -650,6 +663,11 @@ UserSettings::save()
         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();
@@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
         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};
-        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_                     = 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());
@@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
         formLayout_->addRow(new HorizontalLine{this});
         boxWrap(tr("Device ID"), deviceIdValue_);
         boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
-        boxWrap(
-          tr("Share keys with verified users and devices"),
-          shareKeysWithTrustedUsers_,
-          tr("Automatically replies to key requests from other users, if they are verified."));
+        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);
@@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
                 }
         });
 
+        connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) {
+                settings_->setOnlyShareKeysWithVerifiedUsers(enabled);
+        });
+
         connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) {
                 settings_->setShareKeysWithTrustedUsers(enabled);
         });
@@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *)
         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());
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index acb08569..096aab81 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -88,6 +88,8 @@ class UserSettings : public QObject
                      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)
@@ -152,6 +154,7 @@ public:
         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);
@@ -208,6 +211,7 @@ public:
         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_; }
@@ -252,6 +256,7 @@ signals:
         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);
@@ -284,6 +289,7 @@ private:
         bool privacyScreen_;
         int privacyScreenTimeout_;
         bool shareKeysWithTrustedUsers_;
+        bool onlyShareKeysWithVerifiedUsers_;
         bool mobileMode_;
         int timelineMaxWidth_;
         int roomListWidth_;
@@ -372,6 +378,7 @@ private:
         Toggle *privacyScreen_;
         QSpinBox *privacyScreenTimeout_;
         Toggle *shareKeysWithTrustedUsers_;
+        Toggle *onlyShareKeysWithVerifiedUsers_;
         Toggle *mobileMode_;
         QLabel *deviceFingerprintValue_;
         QLabel *deviceIdValue_;
diff --git a/src/dialogs/RawMessage.h b/src/dialogs/RawMessage.h
deleted file mode 100644
index e95f675c..00000000
--- a/src/dialogs/RawMessage.h
+++ /dev/null
@@ -1,60 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFont>
-#include <QFontDatabase>
-#include <QTextBrowser>
-#include <QVBoxLayout>
-#include <QWidget>
-
-#include "nlohmann/json.hpp"
-
-#include "Logging.h"
-#include "MainWindow.h"
-#include "ui/FlatButton.h"
-
-namespace dialogs {
-
-class RawMessage : public QWidget
-{
-        Q_OBJECT
-public:
-        RawMessage(QString msg, QWidget *parent = nullptr)
-          : QWidget{parent}
-        {
-                QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
-
-                auto layout = new QVBoxLayout{this};
-                auto viewer = new QTextBrowser{this};
-                viewer->setFont(monospaceFont);
-                viewer->setText(msg);
-
-                layout->setSpacing(0);
-                layout->setMargin(0);
-                layout->addWidget(viewer);
-
-                setAutoFillBackground(true);
-                setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-                setAttribute(Qt::WA_DeleteOnClose, true);
-
-                QSize winsize;
-                QPoint center;
-
-                auto window = MainWindow::instance();
-                if (window) {
-                        winsize = window->frameGeometry().size();
-                        center  = window->frameGeometry().center();
-
-                        move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
-                } else {
-                        nhlog::ui()->warn("unable to retrieve MainWindow's size");
-                }
-
-                raise();
-                show();
-        }
-};
-} // namespace dialogs
diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp
deleted file mode 100644
index fa7132fd..00000000
--- a/src/dialogs/ReadReceipts.cpp
+++ /dev/null
@@ -1,179 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QDebug>
-#include <QIcon>
-#include <QLabel>
-#include <QListWidgetItem>
-#include <QPainter>
-#include <QPushButton>
-#include <QShortcut>
-#include <QStyleOption>
-#include <QTimer>
-#include <QVBoxLayout>
-
-#include "dialogs/ReadReceipts.h"
-
-#include "AvatarProvider.h"
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-
-using namespace dialogs;
-
-ReceiptItem::ReceiptItem(QWidget *parent,
-                         const QString &user_id,
-                         uint64_t timestamp,
-                         const QString &room_id)
-  : QWidget(parent)
-{
-        topLayout_ = new QHBoxLayout(this);
-        topLayout_->setMargin(0);
-
-        textLayout_ = new QVBoxLayout;
-        textLayout_->setMargin(0);
-        textLayout_->setSpacing(conf::modals::TEXT_SPACING);
-
-        QFont nameFont;
-        nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
-
-        auto displayName = cache::displayName(room_id, user_id);
-
-        avatar_ = new Avatar(this, 44);
-        avatar_->setLetter(utils::firstChar(displayName));
-
-        // If it's a matrix id we use the second letter.
-        if (displayName.size() > 1 && displayName.at(0) == '@')
-                avatar_->setLetter(QChar(displayName.at(1)));
-
-        userName_ = new QLabel(displayName, this);
-        userName_->setFont(nameFont);
-
-        timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this);
-
-        textLayout_->addWidget(userName_);
-        textLayout_->addWidget(timestamp_);
-
-        topLayout_->addWidget(avatar_);
-        topLayout_->addLayout(textLayout_, 1);
-
-        avatar_->setImage(ChatPage::instance()->currentRoom(), user_id);
-}
-
-void
-ReceiptItem::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-QString
-ReceiptItem::dateFormat(const QDateTime &then) const
-{
-        auto now  = QDateTime::currentDateTime();
-        auto days = then.daysTo(now);
-
-        if (days == 0)
-                return tr("Today %1")
-                  .arg(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)
-                return QString("%1 %2")
-                  .arg(then.toString("dddd"))
-                  .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
-
-        return QLocale::system().toString(then.time(), QLocale::ShortFormat);
-}
-
-ReadReceipts::ReadReceipts(QWidget *parent)
-  : QFrame(parent)
-{
-        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);
-
-        userList_ = new QListWidget;
-        userList_->setFrameStyle(QFrame::NoFrame);
-        userList_->setSelectionMode(QAbstractItemView::NoSelection);
-        userList_->setSpacing(conf::modals::TEXT_SPACING);
-
-        QFont largeFont;
-        largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
-
-        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-        setMinimumHeight(userList_->sizeHint().height() * 2);
-        setMinimumWidth(std::max(userList_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN,
-                                 QFontMetrics(largeFont).averageCharWidth() * 30 -
-                                   2 * conf::modals::WIDGET_MARGIN));
-
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
-
-        topLabel_ = new QLabel(tr("Read receipts"), this);
-        topLabel_->setAlignment(Qt::AlignCenter);
-        topLabel_->setFont(font);
-
-        auto okBtn = new QPushButton(tr("Close"), this);
-
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(15);
-        buttonLayout->addStretch(1);
-        buttonLayout->addWidget(okBtn);
-
-        layout->addWidget(topLabel_);
-        layout->addWidget(userList_);
-        layout->addLayout(buttonLayout);
-
-        auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
-        connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close);
-        connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close);
-}
-
-void
-ReadReceipts::addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &receipts)
-{
-        // We want to remove any previous items that have been set.
-        userList_->clear();
-
-        for (const auto &receipt : receipts) {
-                auto user = new ReceiptItem(this,
-                                            QString::fromStdString(receipt.second),
-                                            receipt.first,
-                                            ChatPage::instance()->currentRoom());
-                auto item = new QListWidgetItem(userList_);
-
-                item->setSizeHint(user->minimumSizeHint());
-                item->setFlags(Qt::NoItemFlags);
-                item->setTextAlignment(Qt::AlignCenter);
-
-                userList_->setItemWidget(item, user);
-        }
-}
-
-void
-ReadReceipts::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-ReadReceipts::hideEvent(QHideEvent *event)
-{
-        userList_->clear();
-        QFrame::hideEvent(event);
-}
diff --git a/src/dialogs/ReadReceipts.h b/src/dialogs/ReadReceipts.h
deleted file mode 100644
index 5c6c5d2b..00000000
--- a/src/dialogs/ReadReceipts.h
+++ /dev/null
@@ -1,61 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QDateTime>
-#include <QFrame>
-
-class Avatar;
-class QLabel;
-class QListWidget;
-class QHBoxLayout;
-class QVBoxLayout;
-
-namespace dialogs {
-
-class ReceiptItem : public QWidget
-{
-        Q_OBJECT
-
-public:
-        ReceiptItem(QWidget *parent,
-                    const QString &user_id,
-                    uint64_t timestamp,
-                    const QString &room_id);
-
-protected:
-        void paintEvent(QPaintEvent *) override;
-
-private:
-        QString dateFormat(const QDateTime &then) const;
-
-        QHBoxLayout *topLayout_;
-        QVBoxLayout *textLayout_;
-
-        Avatar *avatar_;
-
-        QLabel *userName_;
-        QLabel *timestamp_;
-};
-
-class ReadReceipts : public QFrame
-{
-        Q_OBJECT
-public:
-        explicit ReadReceipts(QWidget *parent = nullptr);
-
-public slots:
-        void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void hideEvent(QHideEvent *event) override;
-
-private:
-        QLabel *topLabel_;
-
-        QListWidget *userList_;
-};
-} // dialogs
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 9a91ff79..742f8dbb 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -20,8 +20,7 @@
 
 Q_DECLARE_METATYPE(Reaction)
 
-QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{
-  1000};
+QCache<EventStore::IdIndex, olm::DecryptionResult> EventStore::decryptedEvents_{1000};
 QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
   1000};
 QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::events_{1000};
@@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *)
                                                             mtx::events::msg::Encrypted>) {
                                                     auto event =
                                                       decryptEvent({room_id_, e.event_id}, e);
-                                                    if (auto dec =
-                                                          std::get_if<mtx::events::RoomEvent<
-                                                            mtx::events::msg::
-                                                              KeyVerificationRequest>>(event)) {
-                                                            emit updateFlowEventId(
-                                                              event_id.event_id.to_string());
+                                                    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());
+                                                            }
                                                     }
                                             }
                                     });
@@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
                 if (auto encrypted =
                       std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
                         &event)) {
-                        mtx::events::collections::TimelineEvents *d_event =
-                          decryptEvent({room_id_, encrypted->event_id}, *encrypted);
-                        if (std::visit(
+                        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)) {
-                                handle_room_verification(*d_event);
+                              *d_event->event)) {
+                                handle_room_verification(*d_event->event);
                         }
                 }
         }
@@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt)
                 events_.insert(index, event_ptr);
         }
 
-        if (decrypt)
+        if (decrypt) {
                 if (auto encrypted =
                       std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                        event_ptr))
-                        return decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+                        event_ptr)) {
+                        auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+                        if (decrypted->event)
+                                return &*decrypted->event;
+                }
+        }
 
         return event_ptr;
 }
@@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const
         return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
 }
 
-mtx::events::collections::TimelineEvents *
+olm::DecryptionResult *
 EventStore::decryptEvent(const IdIndex &idx,
                          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
 {
@@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx,
         index.session_id = e.content.session_id;
         index.sender_key = e.content.sender_key;
 
-        auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) {
-                auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event));
+        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);
 
-        mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
-        dummy.origin_server_ts = e.origin_server_ts;
-        dummy.event_id         = e.event_id;
-        dummy.sender           = e.sender;
-
         if (decryptionResult.error) {
-                switch (*decryptionResult.error) {
+                switch (decryptionResult.error) {
                 case olm::DecryptionErrorCode::MissingSession:
                 case olm::DecryptionErrorCode::MissingSessionIndex: {
-                        if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession)
-                                dummy.content.body =
-                                  tr("-- Encrypted Event (No keys found for decryption) --",
-                                     "Placeholder, when the message was not decrypted yet or can't "
-                                     "be "
-                                     "decrypted.")
-                                    .toStdString();
-                        else
-                                dummy.content.body =
-                                  tr("-- Encrypted Event (Key not valid for this index) --",
-                                     "Placeholder, when the message can't be decrypted with this "
-                                     "key since it is not valid for this index ")
-                                    .toStdString();
                         nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
                                               index.room_id,
                                               index.session_id,
                                               e.sender);
-                        // we may not want to request keys during initial sync and such
-                        if (suppressKeyRequests)
-                                break;
-                        // TODO: Check if this actually works and look in key backup
-                        auto copy    = e;
-                        copy.room_id = room_id_;
-                        if (pending_key_requests.count(e.content.session_id)) {
-                                pending_key_requests.at(e.content.session_id)
-                                  .events.push_back(copy);
-                        } else {
-                                PendingKeyRequests request;
-                                request.request_id =
-                                  "key_request." + http::client()->generate_txn_id();
-                                request.events.push_back(copy);
-                                olm::send_key_request_for(copy, request.request_id);
-                                pending_key_requests[e.content.session_id] = request;
-                        }
+
+                        requestSession(e, false);
                         break;
                 }
                 case olm::DecryptionErrorCode::DbError:
@@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx,
                           index.session_id,
                           index.sender_key,
                           decryptionResult.error_message.value_or(""));
-                        dummy.content.body =
-                          tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
-                             "Placeholder, when the message can't be decrypted, because the DB "
-                             "access "
-                             "failed.")
-                            .toStdString();
                         break;
                 case olm::DecryptionErrorCode::DecryptionFailed:
                         nhlog::crypto()->critical(
@@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx,
                           index.session_id,
                           index.sender_key,
                           decryptionResult.error_message.value_or(""));
-                        dummy.content.body =
-                          tr("-- Decryption Error (%1) --",
-                             "Placeholder, when the message can't be decrypted. In this case, the "
-                             "Olm "
-                             "decrytion returned an error, which is passed as %1.")
-                            .arg(
-                              QString::fromStdString(decryptionResult.error_message.value_or("")))
-                            .toStdString();
                         break;
                 case olm::DecryptionErrorCode::ParsingFailed:
-                        dummy.content.body =
-                          tr("-- Encrypted Event (Unknown event type) --",
-                             "Placeholder, when the message was decrypted, but we couldn't parse "
-                             "it, because "
-                             "Nheko/mtxclient don't support that event type yet.")
-                            .toStdString();
                         break;
                 case olm::DecryptionErrorCode::ReplayAttack:
                         nhlog::crypto()->critical(
@@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx,
                           e.event_id,
                           room_id_,
                           index.sender_key);
-                        dummy.content.body =
-                          tr("-- Replay attack! This message index was reused! --").toStdString();
                         break;
-                case olm::DecryptionErrorCode::UnknownFingerprint:
-                        // TODO: don't fail, just show in UI.
-                        nhlog::crypto()->critical("Message by unverified fingerprint {}",
-                                                  index.sender_key);
-                        dummy.content.body =
-                          tr("-- Message by unverified device! --").toStdString();
+                case olm::DecryptionErrorCode::NoError:
+                        // unreachable
                         break;
                 }
-                return asCacheEntry(std::move(dummy));
-        }
-
-        std::string msg_str;
-        try {
-                auto session = cache::client()->getInboundMegolmSession(index);
-                auto res =
-                  olm::client()->decrypt_group_message(session.get(), e.content.ciphertext);
-                msg_str = std::string((char *)res.data.data(), res.data.size());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
-                                      index.room_id,
-                                      index.session_id,
-                                      index.sender_key,
-                                      e.what());
-                dummy.content.body =
-                  tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
-                     "Placeholder, when the message can't be decrypted, because the DB "
-                     "access "
-                     "failed.")
-                    .toStdString();
-                return asCacheEntry(std::move(dummy));
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
-                                          index.room_id,
-                                          index.session_id,
-                                          index.sender_key,
-                                          e.what());
-                dummy.content.body =
-                  tr("-- Decryption Error (%1) --",
-                     "Placeholder, when the message can't be decrypted. In this case, the "
-                     "Olm "
-                     "decrytion returned an error, which is passed as %1.")
-                    .arg(e.what())
-                    .toStdString();
-                return asCacheEntry(std::move(dummy));
-        }
-
-        // Add missing fields for the event.
-        json body                = json::parse(msg_str);
-        body["event_id"]         = e.event_id;
-        body["sender"]           = e.sender;
-        body["origin_server_ts"] = e.origin_server_ts;
-        body["unsigned"]         = e.unsigned_data;
-
-        // relations are unencrypted in content...
-        mtx::common::add_relations(body["content"], e.content.relations);
-
-        json event_array = json::array();
-        event_array.push_back(body);
-
-        std::vector<mtx::events::collections::TimelineEvents> temp_events;
-        mtx::responses::utils::parse_timeline_events(event_array, temp_events);
-
-        if (temp_events.size() == 1) {
-                auto encInfo = mtx::accessors::file(temp_events[0]);
-
-                if (encInfo)
-                        emit newEncryptedImage(encInfo.value());
-
-                return asCacheEntry(std::move(temp_events[0]));
+                return asCacheEntry(std::move(decryptionResult));
         }
 
         auto encInfo = mtx::accessors::file(decryptionResult.event.value());
         if (encInfo)
                 emit newEncryptedImage(encInfo.value());
 
-        return asCacheEntry(std::move(decryptionResult.event.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;
+        }
 }
 
 void
@@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool
                 events_by_id_.insert(index, event_ptr);
         }
 
-        if (decrypt)
+        if (decrypt) {
                 if (auto encrypted =
                       std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
-                        event_ptr))
-                        return decryptEvent(index, *encrypted);
+                        event_ptr)) {
+                        auto decrypted = decryptEvent(index, *encrypted);
+                        if (decrypted->event)
+                                return &*decrypted->event;
+                }
+        }
 
         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 (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);
+        }
+
+        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;
+        }
+
+        return olm::DecryptionErrorCode::NoError;
+}
+
 void
 EventStore::fetchMore()
 {
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index 7c404102..59c1c7c0 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -15,6 +15,7 @@
 #include <mtx/responses/messages.hpp>
 #include <mtx/responses/sync.hpp>
 
+#include "Olm.h"
 #include "Reaction.h"
 
 class EventStore : public QObject
@@ -78,6 +79,9 @@ public:
         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
         {
@@ -119,7 +123,7 @@ public slots:
 
 private:
         std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
-        mtx::events::collections::TimelineEvents *decryptEvent(
+        olm::DecryptionResult *decryptEvent(
           const IdIndex &idx,
           const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
         void handle_room_verification(mtx::events::collections::TimelineEvents event);
@@ -129,7 +133,7 @@ private:
         uint64_t first = std::numeric_limits<uint64_t>::max(),
                  last  = std::numeric_limits<uint64_t>::max();
 
-        static QCache<IdIndex, mtx::events::collections::TimelineEvents> decryptedEvents_;
+        static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_;
         static QCache<Index, mtx::events::collections::TimelineEvents> events_;
         static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
 
@@ -137,6 +141,7 @@ private:
         {
                 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;
 
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index f7f377fb..f4c927ac 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -533,6 +533,8 @@ RoomlistModel::initializeRooms()
         for (const auto &id : cache::client()->roomIds())
                 addRoom(id, true);
 
+        nhlog::db()->info("Restored {} rooms from cache", rowCount());
+
         endResetModel();
 }
 
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index ee5564a5..99e00a67 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -28,9 +28,9 @@
 #include "MemberList.h"
 #include "MxcImageProvider.h"
 #include "Olm.h"
+#include "ReadReceiptsModel.h"
 #include "TimelineViewManager.h"
 #include "Utils.h"
-#include "dialogs/RawMessage.h"
 
 Q_DECLARE_METATYPE(QModelIndex)
 
@@ -308,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
         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::ImagePackRooms;
+        //! 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;
         };
@@ -443,6 +452,7 @@ TimelineModel::roleNames() const
           {IsEditable, "isEditable"},
           {IsEncrypted, "isEncrypted"},
           {Trustlevel, "trustlevel"},
+          {EncryptionError, "encryptionError"},
           {ReplyTo, "replyTo"},
           {Reactions, "reactions"},
           {RoomId, "roomId"},
@@ -630,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                 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: {
@@ -681,6 +694,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                 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);
         }
@@ -1025,14 +1039,13 @@ TimelineModel::formatDateSeparator(QDate date) const
 }
 
 void
-TimelineModel::viewRawMessage(QString id) const
+TimelineModel::viewRawMessage(QString id)
 {
         auto e = events.get(id.toStdString(), "", false);
         if (!e)
                 return;
         std::string ev = mtx::accessors::serialize_event(*e).dump(4);
-        auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
-        Q_UNUSED(dialog);
+        emit showRawMessageDialog(QString::fromStdString(ev));
 }
 
 void
@@ -1046,15 +1059,14 @@ TimelineModel::forwardMessage(QString eventId, QString roomId)
 }
 
 void
-TimelineModel::viewDecryptedRawMessage(QString id) const
+TimelineModel::viewDecryptedRawMessage(QString id)
 {
         auto e = events.get(id.toStdString(), "");
         if (!e)
                 return;
 
         std::string ev = mtx::accessors::serialize_event(*e).dump(4);
-        auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
-        Q_UNUSED(dialog);
+        emit showRawMessageDialog(QString::fromStdString(ev));
 }
 
 void
@@ -1089,9 +1101,9 @@ TimelineModel::relatedInfo(QString id)
 }
 
 void
-TimelineModel::readReceiptsAction(QString id) const
+TimelineModel::showReadReceipts(QString id)
 {
-        MainWindow::instance()->openReadReceiptsDialog(id);
+        emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
 }
 
 void
@@ -1545,6 +1557,17 @@ TimelineModel::scrollTimerEvent()
 }
 
 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);
+        }
+}
+
+void
 TimelineModel::copyLinkToEvent(QString eventId) const
 {
         QStringList vias;
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 0e2ce153..ad7cfbbb 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -20,6 +20,7 @@
 #include "InviteesModel.h"
 #include "MemberList.h"
 #include "Permissions.h"
+#include "ReadReceiptsModel.h"
 #include "ui/RoomSettings.h"
 #include "ui/UserProfile.h"
 
@@ -106,7 +107,13 @@ enum EventType
         KeyVerificationCancel,
         KeyVerificationKey,
         KeyVerificationDone,
-        KeyVerificationReady
+        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);
@@ -205,6 +212,7 @@ public:
                 IsEditable,
                 IsEncrypted,
                 Trustlevel,
+                EncryptionError,
                 ReplyTo,
                 Reactions,
                 RoomId,
@@ -235,13 +243,13 @@ public:
         Q_INVOKABLE QString formatGuestAccessEvent(QString id);
         Q_INVOKABLE QString formatPowerLevelEvent(QString id);
 
-        Q_INVOKABLE void viewRawMessage(QString id) const;
+        Q_INVOKABLE void viewRawMessage(QString id);
         Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
-        Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
+        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 readReceiptsAction(QString id) const;
+        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;
@@ -257,6 +265,8 @@ public:
                 endResetModel();
         }
 
+        Q_INVOKABLE void requestKeyForEvent(QString id);
+
         std::vector<::Reaction> reactions(const std::string &event_id)
         {
                 auto list = events.reactions(event_id);
@@ -348,6 +358,8 @@ signals:
         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);
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index fd5528d8..da68d503 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -162,6 +162,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                                          "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",
@@ -210,6 +212,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           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>(
diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp
deleted file mode 100644
index 154a0e2c..00000000
--- a/src/ui/Avatar.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QPainter>
-#include <QPainterPath>
-#include <QSettings>
-
-#include "AvatarProvider.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-
-Avatar::Avatar(QWidget *parent, int size)
-  : QWidget(parent)
-  , size_(size)
-{
-        type_   = ui::AvatarType::Letter;
-        letter_ = "A";
-
-        QFont _font(font());
-        _font.setPointSizeF(ui::FontSize);
-        setFont(_font);
-
-        QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
-        setSizePolicy(policy);
-}
-
-QColor
-Avatar::textColor() const
-{
-        if (!text_color_.isValid())
-                return QColor("black");
-
-        return text_color_;
-}
-
-QColor
-Avatar::backgroundColor() const
-{
-        if (!text_color_.isValid())
-                return QColor("white");
-
-        return background_color_;
-}
-
-QSize
-Avatar::sizeHint() const
-{
-        return QSize(size_ + 2, size_ + 2);
-}
-
-void
-Avatar::setTextColor(const QColor &color)
-{
-        text_color_ = color;
-}
-
-void
-Avatar::setBackgroundColor(const QColor &color)
-{
-        background_color_ = color;
-}
-
-void
-Avatar::setLetter(const QString &letter)
-{
-        letter_ = letter;
-        type_   = ui::AvatarType::Letter;
-        update();
-}
-
-void
-Avatar::setImage(const QString &avatar_url)
-{
-        avatar_url_ = avatar_url;
-        AvatarProvider::resolve(avatar_url,
-                                static_cast<int>(size_ * pixmap_.devicePixelRatio()),
-                                this,
-                                [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
-                                        if (pm.isNull())
-                                                return;
-                                        type_   = ui::AvatarType::Image;
-                                        pixmap_ = pm;
-                                        pixmap_.setDevicePixelRatio(requestedRatio);
-                                        update();
-                                });
-}
-
-void
-Avatar::setImage(const QString &room, const QString &user)
-{
-        room_ = room;
-        user_ = user;
-        AvatarProvider::resolve(room,
-                                user,
-                                static_cast<int>(size_ * pixmap_.devicePixelRatio()),
-                                this,
-                                [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
-                                        if (pm.isNull())
-                                                return;
-                                        type_   = ui::AvatarType::Image;
-                                        pixmap_ = pm;
-                                        pixmap_.setDevicePixelRatio(requestedRatio);
-                                        update();
-                                });
-}
-
-void
-Avatar::setDevicePixelRatio(double ratio)
-{
-        if (type_ == ui::AvatarType::Image && abs(pixmap_.devicePixelRatio() - ratio) > 0.01) {
-                pixmap_ = pixmap_.scaled(QSize(size_, size_) * ratio);
-                pixmap_.setDevicePixelRatio(ratio);
-
-                if (!avatar_url_.isEmpty())
-                        setImage(avatar_url_);
-                else
-                        setImage(room_, user_);
-        }
-}
-
-void
-Avatar::paintEvent(QPaintEvent *)
-{
-        bool rounded = QSettings().value(QStringLiteral("user/avatar_circles"), true).toBool();
-
-        QPainter painter(this);
-
-        painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform |
-                               QPainter::TextAntialiasing);
-
-        QRectF r     = rect();
-        const int hs = size_ / 2;
-
-        if (type_ != ui::AvatarType::Image) {
-                QBrush brush;
-                brush.setStyle(Qt::SolidPattern);
-                brush.setColor(backgroundColor());
-
-                painter.setPen(Qt::NoPen);
-                painter.setBrush(brush);
-                rounded ? painter.drawEllipse(r) : painter.drawRoundedRect(r, 3, 3);
-        } else if (painter.isActive()) {
-                setDevicePixelRatio(painter.device()->devicePixelRatioF());
-        }
-
-        switch (type_) {
-        case ui::AvatarType::Image: {
-                QPainterPath ppath;
-
-                rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_)
-                        : ppath.addRoundedRect(r, 3, 3);
-
-                painter.setClipPath(ppath);
-                painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_),
-                                   pixmap_);
-                break;
-        }
-        case ui::AvatarType::Letter: {
-                painter.setPen(textColor());
-                painter.setBrush(Qt::NoBrush);
-                painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_);
-                break;
-        }
-        default:
-                break;
-        }
-}
diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h
deleted file mode 100644
index bbf05be3..00000000
--- a/src/ui/Avatar.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QImage>
-#include <QPixmap>
-#include <QWidget>
-
-#include "Theme.h"
-
-class Avatar : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-
-public:
-        explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize);
-
-        void setBackgroundColor(const QColor &color);
-        void setImage(const QString &avatar_url);
-        void setImage(const QString &room, const QString &user);
-        void setLetter(const QString &letter);
-        void setTextColor(const QColor &color);
-        void setDevicePixelRatio(double ratio);
-
-        QColor backgroundColor() const;
-        QColor textColor() const;
-
-        QSize sizeHint() const override;
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-private:
-        void init();
-
-        ui::AvatarType type_;
-        QString letter_;
-        QString avatar_url_, room_, user_;
-        QColor background_color_;
-        QColor text_color_;
-        QPixmap pixmap_;
-        int size_;
-};
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index fea10839..9e0d706b 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -6,6 +6,7 @@
 
 #include <QDesktopServices>
 #include <QUrl>
+#include <QWindow>
 
 #include "Cache_p.h"
 #include "ChatPage.h"
@@ -140,3 +141,9 @@ Nheko::openJoinRoomDialog() const
         MainWindow::instance()->openJoinRoomDialog(
           [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
 }
+
+void
+Nheko::reparent(QWindow *win) const
+{
+        win->setTransientParent(MainWindow::instance()->windowHandle());
+}
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 14135fd1..d4d119dc 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -4,12 +4,15 @@
 
 #pragma once
 
+#include <QFontDatabase>
 #include <QObject>
 #include <QPalette>
 
 #include "Theme.h"
 #include "UserProfile.h"
 
+class QWindow;
+
 class Nheko : public QObject
 {
         Q_OBJECT
@@ -38,12 +41,17 @@ public:
         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;
 
 public slots:
         void updateUserProfile();