summary refs log tree commit diff
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2021-08-06 01:45:47 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2021-08-06 04:31:53 +0200
commita57a15a2e07da8cc07bc12e828b7c636efe36cbc (patch)
treea33d6375ab1c24ded5cdf3f628af29bfe8370a68
parentMerge pull request #666 from LorenDB/qml-all-the-things (diff)
downloadnheko-a57a15a2e07da8cc07bc12e828b7c636efe36cbc.tar.xz
Basic sticker pack editor
-rw-r--r--CMakeLists.txt2
-rw-r--r--io.github.NhekoReborn.Nheko.yaml2
-rw-r--r--resources/qml/Avatar.qml6
-rw-r--r--resources/qml/RoomSettings.qml4
-rw-r--r--resources/qml/ScrollHelper.qml7
-rw-r--r--resources/qml/components/AvatarListTile.qml133
-rw-r--r--resources/qml/dialogs/ImagePackEditorDialog.qml283
-rw-r--r--resources/qml/dialogs/ImagePackSettingsDialog.qml174
-rw-r--r--resources/res.qrc2
-rw-r--r--src/Cache.cpp2
-rw-r--r--src/Cache_p.h34
-rw-r--r--src/MxcImageProvider.cpp26
-rw-r--r--src/MxcImageProvider.h7
-rw-r--r--src/SingleImagePackModel.cpp181
-rw-r--r--src/SingleImagePackModel.h38
-rw-r--r--src/timeline/TimelineModel.cpp9
-rw-r--r--src/timeline/TimelineModel.h8
17 files changed, 751 insertions, 167 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9f824048..e8bc855d 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 0fa450b3..2c0c5ebf 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 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/RoomSettings.qml b/resources/qml/RoomSettings.qml
index 6ba080c4..69cf427c 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 2dd56f27..e584ae3d 100644
--- a/resources/qml/ScrollHelper.qml
+++ b/resources/qml/ScrollHelper.qml
@@ -30,6 +30,10 @@ MouseArea {
     property alias enabled: root.enabled
 
     function calculateNewPosition(flickableItem, wheel) {
+        // breaks ListView's with headers...
+        //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
+        //    minYExtent += flickableItem.headerItem.height;
+
         //Nothing to scroll
         if (flickableItem.contentHeight < flickableItem.height)
             return flickableItem.contentY;
@@ -55,9 +59,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 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/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml
new file mode 100644
index 00000000..0049d3b4
--- /dev/null
+++ b/resources/qml/dialogs/ImagePackEditorDialog.qml
@@ -0,0 +1,283 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import "../components"
+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
+                    }
+
+                }
+
+                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
+                        onToggled: imagePack.isEmotePack = checked
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    MatrixText {
+                        text: qsTr("Use as Sticker")
+                    }
+
+                    ToggleButton {
+                        checked: imagePack.isStickerPack
+                        onToggled: 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)
+                        onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsEmote)
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    MatrixText {
+                        text: qsTr("Use as Sticker")
+                    }
+
+                    ToggleButton {
+                        checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker)
+                        onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, 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 3d830bf7..c57867fd 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,7 @@ ApplicationWindow {
                     enabled: !Settings.mobileMode
                 }
 
-                delegate: Rectangle {
+                delegate: AvatarListTile {
                     id: packItem
 
                     property color background: Nheko.colors.window
@@ -63,131 +71,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 +102,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 +123,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 +156,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 +190,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 c911653c..d7187f42 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 291df053..f3f3dbb6 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
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 5d700658..30c365a6 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/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/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp
index 6c508da0..d3cc8014 100644
--- a/src/SingleImagePackModel.cpp
+++ b/src/SingleImagePackModel.cpp
@@ -5,12 +5,18 @@
 #include "SingleImagePackModel.h"
 
 #include "Cache_p.h"
+#include "ChatPage.h"
 #include "MatrixClient.h"
+#include "timeline/Permissions.h"
+#include "timeline/TimelineModel.h"
+
+#include "Logging.h"
 
 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))
 {
         if (!pack.pack)
@@ -62,6 +68,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 +171,111 @@ 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, [this](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(),
+                          [this](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)));
+                  });
+        }
+}
diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h
index e0c791ba..44f413c6 100644
--- a/src/SingleImagePackModel.h
+++ b/src/SingleImagePackModel.h
@@ -15,14 +15,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 +36,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 +55,30 @@ 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();
+
 signals:
         void globallyEnabledChanged();
+        void statekeyChanged();
+        void attributionChanged();
+        void packnameChanged();
+        void avatarUrlChanged();
+        void isEmotePackChanged();
+        void isStickerPackChanged();
 
 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 a8adf05b..10d9788d 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 f62c5360..b5c8ca37 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);