diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9f8240481d0016e9e728d523b7f2966319018ae6..e8bc855d728b4b2f17ba792927525730b6a77b04 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT)
 	FetchContent_Declare(
 		MatrixClient
 		GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-		GIT_TAG        316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
+		GIT_TAG        e5688a2c5987a614b5055595f991f18568127bd2
 		)
 	set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
 	set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml
index 0fa450b32be1537947b87b75f838706d663ce6e7..2c0c5ebf8582fb68edbc69f9df01dc263f12f5c3 100644
--- a/io.github.NhekoReborn.Nheko.yaml
+++ b/io.github.NhekoReborn.Nheko.yaml
@@ -161,7 +161,7 @@ modules:
     buildsystem: cmake-ninja
     name: mtxclient
     sources:
-      - commit: 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
+      - commit: e5688a2c5987a614b5055595f991f18568127bd2
         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 6c12952a2b0e9f345685acda94044018c60ad586..9685dde1bed24e202dff36353b5b0224c5b1782c 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/RoomSettings.qml b/resources/qml/RoomSettings.qml
index 6ba080c4bb96e2cadc7bd6c3297b508a4797339f..69cf427c5abde84575b0b670c137317f92cbae8e 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/RoomSettings.qml
@@ -154,7 +154,7 @@ ApplicationWindow {
 
         GridLayout {
             columns: 2
-            rowSpacing: 10
+            rowSpacing: Nheko.paddingLarge
 
             MatrixText {
                 text: qsTr("SETTINGS")
@@ -180,7 +180,7 @@ ApplicationWindow {
             }
 
             MatrixText {
-                text: "Room access"
+                text: qsTr("Room access")
                 Layout.fillWidth: true
             }
 
diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml
index 2dd56f27c8c9b14a9e9d263d911eb775615d24b8..e84e67fd841f47ec0378f2ebe703ce701c767f32 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/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml
new file mode 100644
index 0000000000000000000000000000000000000000..36c26a9780024ae1ab333c93ecfa76adabce9e05
--- /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/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..b839c9e344b1f4f1c03a0aa2f9353b34fedd35e2
--- /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 3d830bf73c31747d8d9a4676f10500602d3f1225..5181619c03ca620323743d09e12cdfcfad171e15 100644
--- a/resources/qml/dialogs/ImagePackSettingsDialog.qml
+++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml
@@ -20,14 +20,22 @@ ApplicationWindow {
     readonly property int stickerDimPad: 128 + Nheko.paddingSmall
 
     title: qsTr("Image pack settings")
-    height: 400
-    width: 600
+    height: 600
+    width: 800
     palette: Nheko.colors
     color: Nheko.colors.base
     modality: Qt.NonModal
     flags: Qt.Dialog | Qt.WindowCloseButtonHint
     Component.onCompleted: Nheko.reparent(win)
 
+    Component {
+        id: packEditor
+
+        ImagePackEditorDialog {
+        }
+
+    }
+
     AdaptiveLayout {
         id: adaptiveView
 
@@ -54,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
@@ -63,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
-                                }
-
-                            }
-
-                        }
-
-                    }
-
                 }
 
             }
@@ -201,15 +130,10 @@ ApplicationWindow {
                 color: Nheko.colors.window
 
                 ColumnLayout {
-                    //Button {
-                    //    Layout.alignment: Qt.AlignHCenter
-                    //    text: qsTr("Edit")
-                    //    enabled: currentPack.canEdit
-                    //}
-
                     id: packinfo
 
                     property string packName: currentPack ? currentPack.packname : ""
+                    property string attribution: currentPack ? currentPack.attribution : ""
                     property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
 
                     anchors.fill: parent
@@ -227,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 {
@@ -250,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
@@ -272,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/res.qrc b/resources/res.qrc
index c911653ceac6d14b8b7255605b8187d311138cd8..d7187f42ea6c8b8055f4e0e69c28800b1f78d3d2 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -160,6 +160,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>
@@ -173,6 +174,7 @@
         <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>
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 291df053be64b990df4fb5458181690fb412c367..6650334ad57da6aac13ed2a259e78fb43785682f 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
@@ -3401,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;
 
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 5d700658d4d40389d1501c4cbf6160966282f7a2..30c365a62da19ba8fb1c2eb7d42dc475a96e839d 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -291,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)
         {
@@ -416,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());
diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp
index 89f1f68ec72ac6688ce8837108947ac85b15f524..6392de22876807a3d7a589c6069c587c749e2e08 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 0a044690f98d2e8e346264fdb512493d8fdaccc1..2aa5abb25e8200fe0485bdf7cb33f4bc529ef3c2 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/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index ab0f815242d1ef05607cbb1ac6213b42c91ff5e9..b86482698033fb53707055cfbced0e679d581ed1 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 7b960836a28313c0c9aff943c28c536bba844972..61d8285242f3ca8265e17a57fb4056012748dc38 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/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp
index 6c508da006765561ed34a5871dafde4d79a98fb0..7bf556173f30d65778f1dda037df22a0b33becbb 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
@@ -61,6 +76,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const
         return {};
 }
 
+bool
+SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+        using mtx::events::msc2545::PackUsage;
+
+        if (hasIndex(index.row(), index.column(), index.parent())) {
+                auto &img = pack.images.at(shortcodes.at(index.row()));
+                switch (role) {
+                case ShortCode: {
+                        auto newCode = value.toString().toStdString();
+
+                        // otherwise we delete this by accident
+                        if (pack.images.count(newCode))
+                                return false;
+
+                        auto tmp     = img;
+                        auto oldCode = shortcodes.at(index.row());
+                        pack.images.erase(oldCode);
+                        shortcodes[index.row()] = newCode;
+                        pack.images.insert({newCode, tmp});
+
+                        emit dataChanged(
+                          this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
+                        return true;
+                }
+                case Body:
+                        img.body = value.toString().toStdString();
+                        emit dataChanged(
+                          this->index(index.row()), this->index(index.row()), {Roles::Body});
+                        return true;
+                case IsEmote: {
+                        bool isEmote = value.toBool();
+                        bool isSticker =
+                          img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
+
+                        img.usage.set(PackUsage::Emoji, isEmote);
+                        img.usage.set(PackUsage::Sticker, isSticker);
+
+                        if (img.usage == pack.pack->usage)
+                                img.usage.reset();
+
+                        emit dataChanged(
+                          this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
+
+                        return true;
+                }
+                case IsSticker: {
+                        bool isEmote =
+                          img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
+                        bool isSticker = value.toBool();
+
+                        img.usage.set(PackUsage::Emoji, isEmote);
+                        img.usage.set(PackUsage::Sticker, isSticker);
+
+                        if (img.usage == pack.pack->usage)
+                                img.usage.reset();
+
+                        emit dataChanged(
+                          this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
+
+                        return true;
+                }
+                }
+        }
+        return false;
+}
+
 bool
 SingleImagePackModel::isGloballyEnabled() const
 {
@@ -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 e0c791ba0e88f9deb183bfb0ea4e7ab5123a7788..cd38b3b6909e2ca9a0da6e78d13b1b0ffb707471 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/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index a8adf05b5d9cd1a2ff4fc6cdf15c169519190ff6..10d9788d439bfa7eab70f1b2111f1d205a959af0 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -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;
         };
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index f62c5360d8121331c06135ff7215ecb242cf1f58..b5c8ca3737812618e339de028f51d574db510326 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -107,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);