summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/qml/MessageView.qml82
-rw-r--r--resources/qml/delegates/ImageMessage.qml15
-rw-r--r--resources/qml/delegates/MessageDelegate.qml2
-rw-r--r--resources/qml/delegates/TextMessage.qml4
-rw-r--r--src/timeline/EventStore.cpp4
-rw-r--r--src/timeline/TimelineModel.cpp210
-rw-r--r--src/timeline/TimelineModel.h15
7 files changed, 231 insertions, 101 deletions
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 2852e5f7..2d4f592c 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -191,13 +191,40 @@ Item {
                 roleValues: [
                     MtxEvent.TextMessage,
                     MtxEvent.NoticeMessage,
+                    MtxEvent.ElementEffectMessage,
+                    MtxEvent.UnknownMessage,
                 ]
                 TextMessage {
-                    id: textMes
+                    keepFullText: true
+                    required property string userId
+                    required property string userName
+                    required property string formattedBody
+                    required property int type
 
+                    color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text
+                    font.italic: type == MtxEvent.NoticeMessage
+                    formatted: formattedBody
+
+                    Layout.fillWidth: true
+                    //Layout.maximumWidth: implicitWidth
+
+                }
+            }
+
+            EventDelegateChoice {
+                roleValues: [
+                    MtxEvent.EmoteMessage,
+                ]
+                TextMessage {
                     keepFullText: true
                     required property string userId
                     required property string userName
+                    required property string formattedBody
+
+                    formatted: TimelineManager.escapeEmoji(userName) + " " + formattedBody
+
+                    color: TimelineManager.userColor(userId, palette.base)
+                    font.italic: true
 
                     Layout.fillWidth: true
                     //Layout.maximumWidth: implicitWidth
@@ -207,6 +234,59 @@ Item {
 
             EventDelegateChoice {
                 roleValues: [
+                    MtxEvent.CanonicalAlias,
+                    MtxEvent.ServerAcl,
+                    MtxEvent.Name,
+                    MtxEvent.Topic,
+                    MtxEvent.Avatar,
+                    MtxEvent.PinnedEvents,
+                    MtxEvent.ImagePackInRoom,
+                    MtxEvent.SpaceParent,
+                    MtxEvent.RoomCreate,
+                    MtxEvent.PowerLevels,
+                    MtxEvent.PolicyRuleUser,
+                    MtxEvent.PolicyRuleRoom,
+                    MtxEvent.PolicyRuleServer,
+                    MtxEvent.RoomJoinRules,
+                    MtxEvent.RoomHistoryVisibility,
+                    MtxEvent.RoomGuestAccess,
+                ]
+                TextMessage {
+                    keepFullText: true
+
+                    required property string userId
+                    required property string userName
+                    required property string formattedStateEvent
+
+                    isOnlyEmoji: false
+                    text: formattedStateEvent
+                    formatted: ''
+                    body: ''
+
+                    color: palette.buttonText
+                    font.italic: true
+
+                    Layout.fillWidth: true
+                    //Layout.maximumWidth: implicitWidth
+
+                }
+            }
+
+            EventDelegateChoice {
+                roleValues: [
+                    MtxEvent.ImageMessage,
+                    MtxEvent.Sticker,
+                ]
+                ImageMessage {
+                    Layout.fillWidth: true
+
+                    containerHeight: timelineView.height
+                    Layout.maximumWidth: tempWidth
+                }
+            }
+
+            EventDelegateChoice {
+                roleValues: [
                 ]
                 MatrixText {
                     Layout.fillWidth: true
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index 20d727c3..48b7c5e4 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -2,10 +2,11 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import QtQuick 2.15
-import QtQuick.Window 2.15
-import QtQuick.Controls 2.3
-import im.nheko 1.0
+import QtQuick
+import QtQuick.Window
+import QtQuick.Controls
+import QtQuick.Layouts
+import im.nheko
 
 AbstractButton {
     required property int type
@@ -17,13 +18,13 @@ AbstractButton {
     required property string filename
     required property bool isReply
     required property string eventId
+    required property int containerHeight
     property double divisor: isReply ? 5 : 3
 
     property int tempWidth: originalWidth < 1? 400: originalWidth
 
-    implicitWidth: Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1))
-    width: Math.min(parent?.width ?? 2000,implicitWidth)
-    height: width*proportionalHeight
+    Layout.preferredWidth: Math.round(tempWidth*Math.min((containerHeight/divisor)/(tempWidth*proportionalHeight), 1))
+    Layout.preferredHeight: width*proportionalHeight
     hoverEnabled: true
 
     state: (img.status != Image.Ready || timeline.privacyScreen.active) ? "BlurhashVisible" : "ImageVisible"
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 68f65062..44726a63 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -175,6 +175,7 @@ Item {
                 isReply: d.isReply
                 eventId: d.eventId
                 metadataWidth: d.metadataWidth
+                containerHeight: timelineView.height
             }
 
         }
@@ -193,6 +194,7 @@ Item {
                 isReply: d.isReply
                 eventId: d.eventId
                 metadataWidth: d.metadataWidth
+                containerHeight: timelineView.height
             }
 
         }
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index d4b72965..dc8caf01 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -12,7 +12,7 @@ MatrixText {
     required property bool isOnlyEmoji
     required property bool isReply
     required property bool keepFullText
-    required property string formattedBody
+    required property string formatted
 
     property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body
     property int metadataWidth: 100
@@ -40,7 +40,7 @@ MatrixText {
         background-color: " + palette.text + ";
     }" : "") +  // TODO(Nico): Figure out how to support mobile
     "</style>
-    " + formattedBody.replace(/<del>/g, "<s>").replace(/<\/del>/g, "</s>").replace(/<strike>/g, "<s>").replace(/<\/strike>/g, "</s>")
+    " + formatted.replace(/<del>/g, "<s>").replace(/<\/del>/g, "</s>").replace(/<strike>/g, "<s>").replace(/<\/strike>/g, "</s>")
     Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight
     clip: !keepFullText
     selectByMouse: !Settings.mobileMode && !isReply
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 63b67474..e29dfb4c 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -843,8 +843,8 @@ EventStore::get(const std::string &id,
                                               nhlog::net()->error(
                                                 "Failed to retrieve event with id {}, which was "
                                                 "requested to show the replyTo for event {}",
-                                                relatedTo,
-                                                id);
+                                                id,
+                                                relatedTo);
                                               return;
                                           }
                                           emit eventFetched(id, relatedTo, timeline);
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 69ab3f5a..066d8b01 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -532,6 +532,7 @@ TimelineModel::roleNames() const
       {IsOnlyEmoji, "isOnlyEmoji"},
       {Body, "body"},
       {FormattedBody, "formattedBody"},
+      {FormattedStateEvent, "formattedStateEvent"},
       {IsSender, "isSender"},
       {UserId, "userId"},
       {UserName, "userName"},
@@ -694,6 +695,76 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
 
         return QVariant(utils::replaceEmoji(utils::linkifyMessage(formattedBody_)));
     }
+    case FormattedStateEvent: {
+        if (mtx::accessors::is_state_event(event)) {
+            return std::visit(
+              [this](const auto &e) {
+                  constexpr auto t = mtx::events::state_content_to_type<decltype(e.content)>;
+                  if constexpr (t == mtx::events::EventType::RoomServerAcl)
+                      return tr("%1 changed which servers are allowed in this room.")
+                        .arg(displayName(QString::fromStdString(e.sender)));
+                  else if constexpr (t == mtx::events::EventType::RoomName) {
+                      if (e.content.name.empty())
+                          return tr("%1 removed the room name.")
+                            .arg(displayName(QString::fromStdString(e.sender)));
+                      else
+                          return tr("%1 changed the room name to: %2")
+                            .arg(displayName(QString::fromStdString(e.sender)))
+                            .arg(QString::fromStdString(e.content.name).toHtmlEscaped());
+                  } else if constexpr (t == mtx::events::EventType::RoomTopic) {
+                      if (e.content.topic.empty())
+                          return tr("%1 removed the topic.")
+                            .arg(displayName(QString::fromStdString(e.sender)));
+                      else
+                          return tr("%1 changed the topic to: %2")
+                            .arg(displayName(QString::fromStdString(e.sender)))
+                            .arg(QString::fromStdString(e.content.topic).toHtmlEscaped());
+                  } else if constexpr (t == mtx::events::EventType::RoomAvatar) {
+                      if (e.content.url.starts_with("mxc://"))
+                          return tr("%1 changed the room avatar to: %2")
+                            .arg(displayName(QString::fromStdString(e.sender)))
+                            .arg(QStringLiteral("<img height=\"32\" src=\"%1\">")
+                                   .arg(QUrl::toPercentEncoding(
+                                     QString::fromStdString(e.content.url))));
+                      else
+                          return tr("%1 removed the room avatar.")
+                            .arg(displayName(QString::fromStdString(e.sender)));
+                  } else if constexpr (t == mtx::events::EventType::RoomPinnedEvents)
+                      return tr("%1 changed the pinned messages.")
+                        .arg(displayName(QString::fromStdString(e.sender)));
+                  else if constexpr (t == mtx::events::EventType::ImagePackInRoom)
+                      formatImagePackEvent(e);
+                  else if constexpr (t == mtx::events::EventType::RoomCanonicalAlias)
+                      return tr("%1 changed the addresses for this room.")
+                        .arg(displayName(QString::fromStdString(e.sender)));
+                  else if constexpr (t == mtx::events::EventType::SpaceParent)
+                      return tr("%1 changed the parent communities for this room.")
+                        .arg(displayName(QString::fromStdString(e.sender)));
+                  else if constexpr (t == mtx::events::EventType::RoomCreate)
+                      return tr("%1 created and configured room: %2")
+                        .arg(displayName(QString::fromStdString(e.sender)))
+                        .arg(room_id_);
+                  else if constexpr (t == mtx::events::EventType::RoomPowerLevels)
+                      return formatPowerLevelEvent(e);
+                  else if constexpr (t == mtx::events::EventType::PolicyRuleRoom)
+                      return formatPolicyRule(QString::fromStdString(e.event_id));
+                  else if constexpr (t == mtx::events::EventType::PolicyRuleUser)
+                      return formatPolicyRule(QString::fromStdString(e.event_id));
+                  else if constexpr (t == mtx::events::EventType::PolicyRuleServer)
+                      return formatPolicyRule(QString::fromStdString(e.event_id));
+                  else if constexpr (t == mtx::events::EventType::RoomHistoryVisibility)
+                      return formatHistoryVisibilityEvent(e);
+                  else if constexpr (t == mtx::events::EventType::RoomGuestAccess)
+                      return formatGuestAccessEvent(e);
+
+                  return tr("%1 changed unknown state event %2.")
+                    .arg(displayName(QString::fromStdString(e.sender)))
+                    .arg(QString::fromStdString(to_string(e.type)));
+              },
+              event);
+        }
+        return QString();
+    }
     case Url:
         return QVariant(QString::fromStdString(url(event)));
     case ThumbnailUrl:
@@ -2308,20 +2379,13 @@ TimelineModel::formatJoinRuleEvent(const QString &id)
 }
 
 QString
-TimelineModel::formatGuestAccessEvent(const QString &id)
+TimelineModel::formatGuestAccessEvent(
+  const mtx::events::StateEvent<mtx::events::state::GuestAccess> &event) const
 {
-    auto e = events.get(id.toStdString(), "");
-    if (!e)
-        return {};
-
-    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e);
-    if (!event)
-        return {};
-
-    QString user = QString::fromStdString(event->sender);
+    QString user = QString::fromStdString(event.sender);
     QString name = utils::replaceEmoji(displayName(user));
 
-    switch (event->content.guest_access) {
+    switch (event.content.guest_access) {
     case mtx::events::state::AccessState::CanJoin:
         return tr("%1 made the room open to guests.").arg(name);
     case mtx::events::state::AccessState::Forbidden:
@@ -2332,21 +2396,13 @@ TimelineModel::formatGuestAccessEvent(const QString &id)
 }
 
 QString
-TimelineModel::formatHistoryVisibilityEvent(const QString &id)
+TimelineModel::formatHistoryVisibilityEvent(
+  const mtx::events::StateEvent<mtx::events::state::HistoryVisibility> &event) const
 {
-    auto e = events.get(id.toStdString(), "");
-    if (!e)
-        return {};
-
-    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e);
-
-    if (!event)
-        return {};
-
-    QString user = QString::fromStdString(event->sender);
+    QString user = QString::fromStdString(event.sender);
     QString name = utils::replaceEmoji(displayName(user));
 
-    switch (event->content.history_visibility) {
+    switch (event.content.history_visibility) {
     case mtx::events::state::Visibility::WorldReadable:
         return tr("%1 made the room history world readable. Events may be now read by "
                   "non-joined people.")
@@ -2364,32 +2420,25 @@ TimelineModel::formatHistoryVisibilityEvent(const QString &id)
 }
 
 QString
-TimelineModel::formatPowerLevelEvent(const QString &id)
+TimelineModel::formatPowerLevelEvent(
+  const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const
 {
-    auto e = events.get(id.toStdString(), "");
-    if (!e)
-        return {};
-
-    auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e);
-    if (!event)
-        return QString();
-
     mtx::events::StateEvent<mtx::events::state::PowerLevels> const *prevEvent = nullptr;
-    if (!event->unsigned_data.replaces_state.empty()) {
-        auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id);
+    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::state::PowerLevels>>(tempPrevEvent);
         }
     }
 
-    QString user        = QString::fromStdString(event->sender);
+    QString user        = QString::fromStdString(event.sender);
     QString sender_name = utils::replaceEmoji(displayName(user));
     // Get the rooms levels for redactions and powerlevel changes to determine "Administrator" and
     // "Moderator" powerlevels.
-    auto administrator_power_level = event->content.state_level("m.room.power_levels");
-    auto moderator_power_level     = event->content.redact;
-    auto default_powerlevel        = event->content.users_default;
+    auto administrator_power_level = event.content.state_level("m.room.power_levels");
+    auto moderator_power_level     = event.content.redact;
+    auto default_powerlevel        = event.content.users_default;
     if (!prevEvent)
         return tr("%1 has changed the room's permissions.").arg(sender_name);
 
@@ -2399,7 +2448,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
         auto numberOfAffected = 0;
         // We do only compare to people with explicit PL. Usually others are not going to be
         // affected either way and this is cheaper to iterate over.
-        for (auto const &[mxid, currentPowerlevel] : event->content.users) {
+        for (auto const &[mxid, currentPowerlevel] : event.content.users) {
             if (currentPowerlevel == newPowerlevelSetting &&
                 prevEvent->content.user_level(mxid) < newPowerlevelSetting) {
                 numberOfAffected++;
@@ -2413,16 +2462,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
 
     QStringList resultingMessage{};
     // These affect only a few people. Therefor we can print who is affected.
-    if (event->content.kick != prevEvent->content.kick) {
+    if (event.content.kick != prevEvent->content.kick) {
         auto default_message = tr("%1 has changed the room's kick powerlevel from %2 to %3.")
                                  .arg(sender_name)
                                  .arg(prevEvent->content.kick)
-                                 .arg(event->content.kick);
+                                 .arg(event.content.kick);
 
         // We only calculate affected users if we change to a level above the default users PL
         // to not accidentally have a DoS vector
-        if (event->content.kick > default_powerlevel) {
-            auto [affected, number_of_affected] = calc_affected(event->content.kick);
+        if (event.content.kick > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event.content.kick);
 
             if (number_of_affected != 0) {
                 auto true_affected_rest = number_of_affected - affected.size();
@@ -2444,16 +2493,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
         }
     }
 
-    if (event->content.redact != prevEvent->content.redact) {
+    if (event.content.redact != prevEvent->content.redact) {
         auto default_message = tr("%1 has changed the room's redact powerlevel from %2 to %3.")
                                  .arg(sender_name)
                                  .arg(prevEvent->content.redact)
-                                 .arg(event->content.redact);
+                                 .arg(event.content.redact);
 
         // We only calculate affected users if we change to a level above the default users PL
         // to not accidentally have a DoS vector
-        if (event->content.redact > default_powerlevel) {
-            auto [affected, number_of_affected] = calc_affected(event->content.redact);
+        if (event.content.redact > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event.content.redact);
 
             if (number_of_affected != 0) {
                 auto true_affected_rest = number_of_affected - affected.size();
@@ -2476,16 +2525,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
         }
     }
 
-    if (event->content.ban != prevEvent->content.ban) {
+    if (event.content.ban != prevEvent->content.ban) {
         auto default_message = tr("%1 has changed the room's ban powerlevel from %2 to %3.")
                                  .arg(sender_name)
                                  .arg(prevEvent->content.ban)
-                                 .arg(event->content.ban);
+                                 .arg(event.content.ban);
 
         // We only calculate affected users if we change to a level above the default users PL
         // to not accidentally have a DoS vector
-        if (event->content.ban > default_powerlevel) {
-            auto [affected, number_of_affected] = calc_affected(event->content.ban);
+        if (event.content.ban > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event.content.ban);
 
             if (number_of_affected != 0) {
                 auto true_affected_rest = number_of_affected - affected.size();
@@ -2507,17 +2556,17 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
         }
     }
 
-    if (event->content.state_default != prevEvent->content.state_default) {
+    if (event.content.state_default != prevEvent->content.state_default) {
         auto default_message =
           tr("%1 has changed the room's state_default powerlevel from %2 to %3.")
             .arg(sender_name)
             .arg(prevEvent->content.state_default)
-            .arg(event->content.state_default);
+            .arg(event.content.state_default);
 
         // We only calculate affected users if we change to a level above the default users PL
         // to not accidentally have a DoS vector
-        if (event->content.state_default > default_powerlevel) {
-            auto [affected, number_of_affected] = calc_affected(event->content.kick);
+        if (event.content.state_default > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event.content.kick);
 
             if (number_of_affected != 0) {
                 auto true_affected_rest = number_of_affected - affected.size();
@@ -2541,42 +2590,42 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
 
     // These affect potentially the whole room. We there for do not calculate who gets affected
     // by this to prevent huge lists of people.
-    if (event->content.invite != prevEvent->content.invite) {
+    if (event.content.invite != prevEvent->content.invite) {
         resultingMessage.append(tr("%1 has changed the room's invite powerlevel from %2 to %3.")
                                   .arg(sender_name,
                                        QString::number(prevEvent->content.invite),
-                                       QString::number(event->content.invite)));
+                                       QString::number(event.content.invite)));
     }
 
-    if (event->content.events_default != prevEvent->content.events_default) {
-        if ((event->content.events_default > default_powerlevel) &&
+    if (event.content.events_default != prevEvent->content.events_default) {
+        if ((event.content.events_default > default_powerlevel) &&
             prevEvent->content.events_default <= default_powerlevel) {
             resultingMessage.append(
               tr("%1 has changed the room's events_default powerlevel from %2 to %3. New "
                  "users can now not send any events.")
                 .arg(sender_name,
                      QString::number(prevEvent->content.events_default),
-                     QString::number(event->content.events_default)));
-        } else if ((event->content.events_default < prevEvent->content.events_default) &&
-                   (event->content.events_default < default_powerlevel) &&
+                     QString::number(event.content.events_default)));
+        } else if ((event.content.events_default < prevEvent->content.events_default) &&
+                   (event.content.events_default < default_powerlevel) &&
                    (prevEvent->content.events_default > default_powerlevel)) {
             resultingMessage.append(
               tr("%1 has changed the room's events_default powerlevel from %2 to %3. New "
                  "users can now send events that are not otherwise restricted.")
                 .arg(sender_name,
                      QString::number(prevEvent->content.events_default),
-                     QString::number(event->content.events_default)));
+                     QString::number(event.content.events_default)));
         } else {
             resultingMessage.append(
               tr("%1 has changed the room's events_default powerlevel from %2 to %3.")
                 .arg(sender_name,
                      QString::number(prevEvent->content.events_default),
-                     QString::number(event->content.events_default)));
+                     QString::number(event.content.events_default)));
         }
     }
 
     // Compare if a Powerlevel of a user changed
-    for (auto const &[mxid, powerlevel] : event->content.users) {
+    for (auto const &[mxid, powerlevel] : event.content.users) {
         auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid)));
         if (prevEvent->content.user_level(mxid) != powerlevel) {
             if (powerlevel >= administrator_power_level) {
@@ -2601,7 +2650,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
     }
 
     // Handle added/removed/changed event type
-    for (auto const &[event_type, powerlevel] : event->content.events) {
+    for (auto const &[event_type, powerlevel] : event.content.events) {
         auto prev_not_present =
           prevEvent->content.events.find(event_type) == prevEvent->content.events.end();
 
@@ -2640,26 +2689,19 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
 }
 
 QString
-TimelineModel::formatImagePackEvent(const QString &id)
+TimelineModel::formatImagePackEvent(
+  const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const
 {
-    auto 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> const *prevEvent = nullptr;
-    if (!event->unsigned_data.replaces_state.empty()) {
-        auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id);
+    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 &newImages = event.content.images;
     const auto oldImages  = prevEvent ? prevEvent->content.images : decltype(newImages){};
 
     auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
@@ -2682,12 +2724,12 @@ TimelineModel::formatImagePackEvent(const QString &id)
     auto added   = calcChange(newImages, oldImages);
     auto removed = calcChange(oldImages, newImages);
 
-    auto sender       = utils::replaceEmoji(displayName(QString::fromStdString(event->sender)));
+    auto sender       = utils::replaceEmoji(displayName(QString::fromStdString(event.sender)));
     const auto packId = [&event]() -> QString {
-        if (event->content.pack && !event->content.pack->display_name.empty()) {
-            return event->content.pack->display_name.c_str();
-        } else if (!event->state_key.empty()) {
-            return event->state_key.c_str();
+        if (event.content.pack && !event.content.pack->display_name.empty()) {
+            return event.content.pack->display_name.c_str();
+        } else if (!event.state_key.empty()) {
+            return event.state_key.c_str();
         }
         return tr("(empty)");
     }();
@@ -2712,7 +2754,7 @@ TimelineModel::formatImagePackEvent(const QString &id)
 }
 
 QString
-TimelineModel::formatPolicyRule(const QString &id)
+TimelineModel::formatPolicyRule(const QString &id) const
 {
     auto idStr = id.toStdString();
     auto e     = events.get(idStr, "");
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 57caf26d..b81fc209 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -238,6 +238,7 @@ public:
         IsOnlyEmoji,
         Body,
         FormattedBody,
+        FormattedStateEvent,
         IsSender,
         UserId,
         UserName,
@@ -310,11 +311,15 @@ public:
     Q_INVOKABLE void joinReplacementRoom(const QString &id);
     Q_INVOKABLE QString formatMemberEvent(const QString &id);
     Q_INVOKABLE QString formatJoinRuleEvent(const QString &id);
-    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 QString formatPolicyRule(const QString &id);
+    QString formatHistoryVisibilityEvent(
+      const mtx::events::StateEvent<mtx::events::state::HistoryVisibility> &event) const;
+    QString
+    formatGuestAccessEvent(const mtx::events::StateEvent<mtx::events::state::GuestAccess> &) const;
+    QString formatPowerLevelEvent(
+      const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const;
+    QString formatImagePackEvent(
+      const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const;
+    Q_INVOKABLE QString formatPolicyRule(const QString &id) const;
     Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id);
 
     Q_INVOKABLE void viewRawMessage(const QString &id);