diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 0e211ded335393a65106b7c148ae453230cef875..44da16bfcf93fde8813468979dbc1899ea1c507c 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -284,11 +284,12 @@ Item {
                 isOnlyEmoji: false
                 isReply: d.isReply
                 isStateEvent: d.isStateEvent
-                formatted: qsTr("%1 changed the stickers and emotes in this room.").arg(d.userName)
+                formatted: d.relatedEventCacheBuster, room.formatImagePackEvent(d.eventId)
             }
 
         }
 
+
         DelegateChoice {
             roleValue: MtxEvent.CanonicalAlias
 
@@ -390,7 +391,6 @@ Item {
         }
 
         DelegateChoice {
-            // TODO: make a more complex formatter for the power levels.
             roleValue: MtxEvent.PowerLevels
 
             NoticeMessage {
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8d7b7919ff55e182e8c26b3563744be657f66bdf..9c12b967ee2303b4333cdc9b1efab7e3957fc8e0 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -197,6 +197,12 @@ qml_mtx_events::toRoomEventType(mtx::events::EventType e)
         return qml_mtx_events::EventType::SpaceParent;
     case EventType::SpaceChild:
         return qml_mtx_events::EventType::SpaceChild;
+    case EventType::ImagePackInRoom:
+        return qml_mtx_events::ImagePackInRoom;
+    case EventType::ImagePackInAccountData:
+        return qml_mtx_events::ImagePackInAccountData;
+    case EventType::ImagePackRooms:
+        return qml_mtx_events::ImagePackRooms;
     case EventType::Unsupported:
         return qml_mtx_events::EventType::Unsupported;
     default:
@@ -2201,6 +2207,69 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
     }
 }
 
+QString
+TimelineModel::formatImagePackEvent(const QString &id)
+{
+    mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+    if (!e)
+        return {};
+
+    auto event = std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(e);
+    if (!event)
+        return {};
+
+    mtx::events::StateEvent<mtx::events::msc2545::ImagePack> *prevEvent = nullptr;
+    if (!event->unsigned_data.replaces_state.empty()) {
+        auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id);
+        if (tempPrevEvent) {
+            prevEvent =
+              std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(tempPrevEvent);
+        }
+    }
+
+    const auto &newImages = event->content.images;
+    const auto oldImages  = prevEvent ? prevEvent->content.images : decltype(newImages){};
+
+    auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
+
+    auto calcChange = [ascent](const std::map<std::string, mtx::events::msc2545::PackImage> &newI,
+                               const std::map<std::string, mtx::events::msc2545::PackImage> &oldI) {
+        QStringList added;
+        for (const auto &[shortcode, img] : newI) {
+            if (!oldI.count(shortcode))
+                added.push_back(QStringLiteral("<img data-mx-emoticon height=%1 src=\"%2\"> (~%3)")
+                                  .arg(ascent)
+                                  .arg(QString::fromStdString(img.url)
+                                         .replace("mxc://", "image://mxcImage/")
+                                         .toHtmlEscaped(),
+                                       QString::fromStdString(shortcode)));
+        }
+        return added;
+    };
+
+    auto added   = calcChange(newImages, oldImages);
+    auto removed = calcChange(oldImages, newImages);
+
+    auto sender = utils::replaceEmoji(displayName(QString::fromStdString(event->sender)));
+
+    QString msg;
+
+    if (!removed.isEmpty()) {
+        msg = tr("%1 removed the following images from the pack:<br>%2")
+                .arg(sender, removed.join(", "));
+    }
+    if (!added.isEmpty()) {
+        if (!msg.isEmpty())
+            msg += "<br>";
+        msg += tr("%1 added the following images to the pack:<br>%2").arg(sender, added.join(", "));
+    }
+
+    if (msg.isEmpty())
+        return tr("%1 changed the sticker and emotes in this room.").arg(sender);
+    else
+        return msg;
+}
+
 QVariantMap
 TimelineModel::formatRedactedEvent(const QString &id)
 {
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 7e21a394ee300236794f324071ff288d7683126c..c52473b19b78a325fd102e33ee7704926e631e1b 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -262,6 +262,7 @@ public:
     Q_INVOKABLE QString formatHistoryVisibilityEvent(const QString &id);
     Q_INVOKABLE QString formatGuestAccessEvent(const QString &id);
     Q_INVOKABLE QString formatPowerLevelEvent(const QString &id);
+    Q_INVOKABLE QString formatImagePackEvent(const QString &id);
     Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id);
 
     Q_INVOKABLE void viewRawMessage(const QString &id);