diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5c68d614460e1c5e38f533a13fc7b65a62733524..7871bb6a024812205b6b2e81c7d945a6a98ca8e9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -219,6 +219,7 @@ set(SRC_FILES
     src/ChatPage.cpp
     src/CommunitiesListItem.cpp
     src/CommunitiesList.cpp
+    src/EventAccessors.cpp
     src/InviteeItem.cpp
     src/LoginPage.cpp
     src/Logging.cpp
diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6264af587a94e971c36c6d6cef1b7b62eb4a4020
--- /dev/null
+++ b/src/EventAccessors.cpp
@@ -0,0 +1,363 @@
+#include "EventAccessors.h"
+
+#include <type_traits>
+
+namespace {
+struct nonesuch
+{
+        ~nonesuch()                = delete;
+        nonesuch(nonesuch const &) = delete;
+        void operator=(nonesuch const &) = delete;
+};
+
+namespace detail {
+template<class Default, class AlwaysVoid, template<class...> class Op, class... Args>
+struct detector
+{
+        using value_t = std::false_type;
+        using type    = Default;
+};
+
+template<class Default, template<class...> class Op, class... Args>
+struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
+{
+        using value_t = std::true_type;
+        using type    = Op<Args...>;
+};
+
+} // namespace detail
+
+template<template<class...> class Op, class... Args>
+using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t;
+
+struct EventMsgType
+{
+        template<class E>
+        using msgtype_t = decltype(E::msgtype);
+        template<class T>
+        mtx::events::MessageType operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<msgtype_t, T>::value)
+                        return mtx::events::getMessageType(e.content.msgtype);
+                return mtx::events::MessageType::Unknown;
+        }
+};
+
+struct EventRoomName
+{
+        template<class T>
+        std::string operator()(const T &e)
+        {
+                if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Name>, T>)
+                        return e.content.name;
+                return "";
+        }
+};
+
+struct EventRoomTopic
+{
+        template<class T>
+        std::string operator()(const T &e)
+        {
+                if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Topic>, T>)
+                        return e.content.topic;
+                return "";
+        }
+};
+
+struct EventBody
+{
+        template<class C>
+        using body_t = decltype(C::body);
+        template<class T>
+        std::string operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<body_t, T>::value)
+                        return e.content.body;
+                return "";
+        }
+};
+
+struct EventFormattedBody
+{
+        template<class C>
+        using formatted_body_t = decltype(C::formatted_body);
+        template<class T>
+        std::string operator()(const mtx::events::RoomEvent<T> &e)
+        {
+                if constexpr (is_detected<formatted_body_t, T>::value)
+                        return e.content.formatted_body;
+                return "";
+        }
+};
+
+struct EventFile
+{
+        template<class Content>
+        using file_t = decltype(Content::file);
+        template<class T>
+        std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<file_t, T>::value)
+                        return e.content.file;
+                return std::nullopt;
+        }
+};
+
+struct EventUrl
+{
+        template<class Content>
+        using url_t = decltype(Content::url);
+        template<class T>
+        std::string operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<url_t, T>::value) {
+                        if (auto file = EventFile{}(e))
+                                return file->url;
+                        return e.content.url;
+                }
+                return "";
+        }
+};
+
+struct EventThumbnailUrl
+{
+        template<class Content>
+        using thumbnail_url_t = decltype(Content::info.thumbnail_url);
+        template<class T>
+        std::string operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<thumbnail_url_t, T>::value) {
+                        return e.content.info.thumbnail_url;
+                }
+                return "";
+        }
+};
+
+struct EventFilename
+{
+        template<class T>
+        std::string operator()(const mtx::events::Event<T> &)
+        {
+                return "";
+        }
+        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e)
+        {
+                // body may be the original filename
+                return e.content.body;
+        }
+        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Video> &e)
+        {
+                // body may be the original filename
+                return e.content.body;
+        }
+        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Image> &e)
+        {
+                // body may be the original filename
+                return e.content.body;
+        }
+        std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::File> &e)
+        {
+                // body may be the original filename
+                if (!e.content.filename.empty())
+                        return e.content.filename;
+                return e.content.body;
+        }
+};
+
+struct EventMimeType
+{
+        template<class Content>
+        using mimetype_t = decltype(Content::info.mimetype);
+        template<class T>
+        std::string operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<mimetype_t, T>::value) {
+                        return e.content.info.mimetype;
+                }
+                return "";
+        }
+};
+
+struct EventFilesize
+{
+        template<class Content>
+        using filesize_t = decltype(Content::info.size);
+        template<class T>
+        int64_t operator()(const mtx::events::RoomEvent<T> &e)
+        {
+                if constexpr (is_detected<filesize_t, T>::value) {
+                        return e.content.info.size;
+                }
+                return 0;
+        }
+};
+
+struct EventInReplyTo
+{
+        template<class Content>
+        using related_ev_id_t = decltype(Content::relates_to.in_reply_to.event_id);
+        template<class T>
+        std::string operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<related_ev_id_t, T>::value) {
+                        return e.content.relates_to.in_reply_to.event_id;
+                }
+                return "";
+        }
+};
+
+struct EventMediaHeight
+{
+        template<class Content>
+        using h_t = decltype(Content::info.h);
+        template<class T>
+        uint64_t operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<h_t, T>::value) {
+                        return e.content.info.h;
+                }
+                return -1;
+        }
+};
+
+struct EventMediaWidth
+{
+        template<class Content>
+        using w_t = decltype(Content::info.w);
+        template<class T>
+        uint64_t operator()(const mtx::events::Event<T> &e)
+        {
+                if constexpr (is_detected<w_t, T>::value) {
+                        return e.content.info.w;
+                }
+                return -1;
+        }
+};
+
+template<class T>
+double
+eventPropHeight(const mtx::events::RoomEvent<T> &e)
+{
+        auto w = eventWidth(e);
+        if (w == 0)
+                w = 1;
+
+        double prop = eventHeight(e) / (double)w;
+
+        return prop > 0 ? prop : 1.;
+}
+}
+
+std::string
+mtx::accessors::event_id(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit([](const auto e) { return e.event_id; }, event);
+}
+std::string
+mtx::accessors::room_id(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit([](const auto e) { return e.room_id; }, event);
+}
+
+std::string
+mtx::accessors::sender(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit([](const auto e) { return e.sender; }, event);
+}
+
+QDateTime
+mtx::accessors::origin_server_ts(const mtx::events::collections::TimelineEvents &event)
+{
+        return QDateTime::fromMSecsSinceEpoch(
+          std::visit([](const auto e) { return e.origin_server_ts; }, event));
+}
+
+std::string
+mtx::accessors::filename(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventFilename{}, event);
+}
+
+mtx::events::MessageType
+mtx::accessors::msg_type(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventMsgType{}, event);
+}
+std::string
+mtx::accessors::room_name(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventRoomName{}, event);
+}
+std::string
+mtx::accessors::room_topic(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventRoomTopic{}, event);
+}
+
+std::string
+mtx::accessors::body(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventBody{}, event);
+}
+
+std::string
+mtx::accessors::formatted_body(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventFormattedBody{}, event);
+}
+
+QString
+mtx::accessors::formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event)
+{
+        auto formatted = formatted_body(event);
+        if (!formatted.empty())
+                return QString::fromStdString(formatted);
+        else
+                return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "<br>");
+}
+
+std::optional<mtx::crypto::EncryptedFile>
+mtx::accessors::file(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventFile{}, event);
+}
+
+std::string
+mtx::accessors::url(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventUrl{}, event);
+}
+std::string
+mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventThumbnailUrl{}, event);
+}
+std::string
+mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventMimeType{}, event);
+}
+std::string
+mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventInReplyTo{}, event);
+}
+
+int64_t
+mtx::accessors::filesize(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventFilesize{}, event);
+}
+
+uint64_t
+mtx::accessors::media_height(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventMediaHeight{}, event);
+}
+
+uint64_t
+mtx::accessors::media_width(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit(EventMediaWidth{}, event);
+}
diff --git a/src/EventAccessors.h b/src/EventAccessors.h
new file mode 100644
index 0000000000000000000000000000000000000000..5c03861d73b1b727793905674bb8bf255c004519
--- /dev/null
+++ b/src/EventAccessors.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <string>
+
+#include <QDateTime>
+#include <QString>
+
+#include <mtx/events/collections.hpp>
+
+namespace mtx::accessors {
+std::string
+event_id(const mtx::events::collections::TimelineEvents &event);
+
+std::string
+room_id(const mtx::events::collections::TimelineEvents &event);
+
+std::string
+sender(const mtx::events::collections::TimelineEvents &event);
+
+QDateTime
+origin_server_ts(const mtx::events::collections::TimelineEvents &event);
+
+std::string
+filename(const mtx::events::collections::TimelineEvents &event);
+
+mtx::events::MessageType
+msg_type(const mtx::events::collections::TimelineEvents &event);
+std::string
+room_name(const mtx::events::collections::TimelineEvents &event);
+std::string
+room_topic(const mtx::events::collections::TimelineEvents &event);
+
+std::string
+body(const mtx::events::collections::TimelineEvents &event);
+
+std::string
+formatted_body(const mtx::events::collections::TimelineEvents &event);
+
+QString
+formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event);
+
+std::optional<mtx::crypto::EncryptedFile>
+file(const mtx::events::collections::TimelineEvents &event);
+
+std::string
+url(const mtx::events::collections::TimelineEvents &event);
+std::string
+thumbnail_url(const mtx::events::collections::TimelineEvents &event);
+std::string
+mimetype(const mtx::events::collections::TimelineEvents &event);
+std::string
+in_reply_to_event(const mtx::events::collections::TimelineEvents &event);
+
+int64_t
+filesize(const mtx::events::collections::TimelineEvents &event);
+
+uint64_t
+media_height(const mtx::events::collections::TimelineEvents &event);
+
+uint64_t
+media_width(const mtx::events::collections::TimelineEvents &event);
+}
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index ce238d946d8fdcdcc6c197ab023c6bc8e820455d..593a21df0c6579a5fe645c60a859a42ec35145d0 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -9,6 +9,7 @@
 #include <QStandardPaths>
 
 #include "ChatPage.h"
+#include "EventAccessors.h"
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
@@ -21,364 +22,100 @@
 Q_DECLARE_METATYPE(QModelIndex)
 
 namespace {
-template<class T>
-QString
-eventId(const mtx::events::RoomEvent<T> &event)
-{
-        return QString::fromStdString(event.event_id);
-}
-template<class T>
-QString
-roomId(const mtx::events::Event<T> &event)
-{
-        return QString::fromStdString(event.room_id);
-}
-template<class T>
-QString
-senderId(const mtx::events::RoomEvent<T> &event)
-{
-        return QString::fromStdString(event.sender);
-}
-
-template<class T>
-QDateTime
-eventTimestamp(const mtx::events::RoomEvent<T> &event)
-{
-        return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-}
-
-template<class T>
-std::string
-eventMsgType(const mtx::events::Event<T> &)
-{
-        return "";
-}
-template<class T>
-auto
-eventMsgType(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.msgtype)
-{
-        return e.content.msgtype;
-}
-
-template<class T>
-QString
-eventRoomName(const T &)
-{
-        return "";
-}
-QString
-eventRoomName(const mtx::events::StateEvent<mtx::events::state::Name> &e)
-{
-        return QString::fromStdString(e.content.name);
-}
-
-template<class T>
-QString
-eventRoomTopic(const T &)
-{
-        return "";
-}
-QString
-eventRoomTopic(const mtx::events::StateEvent<mtx::events::state::Topic> &e)
-{
-        return QString::fromStdString(e.content.topic);
-}
-
-template<class T>
-QString
-eventBody(const mtx::events::Event<T> &)
-{
-        return QString("");
-}
-template<class T>
-auto
-eventBody(const mtx::events::RoomEvent<T> &e)
-  -> std::enable_if_t<std::is_same<decltype(e.content.body), std::string>::value, QString>
-{
-        return QString::fromStdString(e.content.body);
-}
-
-template<class T>
-QString
-eventFormattedBody(const mtx::events::Event<T> &)
-{
-        return QString("");
-}
-template<class T>
-auto
-eventFormattedBody(const mtx::events::RoomEvent<T> &e)
-  -> std::enable_if_t<std::is_same<decltype(e.content.formatted_body), std::string>::value, QString>
+struct RoomEventType
 {
-        auto temp = e.content.formatted_body;
-        if (!temp.empty()) {
-                return QString::fromStdString(temp);
-        } else {
-                return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "<br>");
+        template<class T>
+        qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e)
+        {
+                using mtx::events::EventType;
+                switch (e.type) {
+                case EventType::RoomKeyRequest:
+                        return qml_mtx_events::EventType::KeyRequest;
+                case EventType::RoomAliases:
+                        return qml_mtx_events::EventType::Aliases;
+                case EventType::RoomAvatar:
+                        return qml_mtx_events::EventType::Avatar;
+                case EventType::RoomCanonicalAlias:
+                        return qml_mtx_events::EventType::CanonicalAlias;
+                case EventType::RoomCreate:
+                        return qml_mtx_events::EventType::Create;
+                case EventType::RoomEncrypted:
+                        return qml_mtx_events::EventType::Encrypted;
+                case EventType::RoomEncryption:
+                        return qml_mtx_events::EventType::Encryption;
+                case EventType::RoomGuestAccess:
+                        return qml_mtx_events::EventType::GuestAccess;
+                case EventType::RoomHistoryVisibility:
+                        return qml_mtx_events::EventType::HistoryVisibility;
+                case EventType::RoomJoinRules:
+                        return qml_mtx_events::EventType::JoinRules;
+                case EventType::RoomMember:
+                        return qml_mtx_events::EventType::Member;
+                case EventType::RoomMessage:
+                        return qml_mtx_events::EventType::UnknownMessage;
+                case EventType::RoomName:
+                        return qml_mtx_events::EventType::Name;
+                case EventType::RoomPowerLevels:
+                        return qml_mtx_events::EventType::PowerLevels;
+                case EventType::RoomTopic:
+                        return qml_mtx_events::EventType::Topic;
+                case EventType::RoomTombstone:
+                        return qml_mtx_events::EventType::Tombstone;
+                case EventType::RoomRedaction:
+                        return qml_mtx_events::EventType::Redaction;
+                case EventType::RoomPinnedEvents:
+                        return qml_mtx_events::EventType::PinnedEvents;
+                case EventType::Sticker:
+                        return qml_mtx_events::EventType::Sticker;
+                case EventType::Tag:
+                        return qml_mtx_events::EventType::Tag;
+                case EventType::Unsupported:
+                default:
+                        return qml_mtx_events::EventType::Unsupported;
+                }
+        }
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &)
+        {
+                return qml_mtx_events::EventType::AudioMessage;
+        }
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &)
+        {
+                return qml_mtx_events::EventType::EmoteMessage;
+        }
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &)
+        {
+                return qml_mtx_events::EventType::FileMessage;
+        }
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &)
+        {
+                return qml_mtx_events::EventType::ImageMessage;
+        }
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &)
+        {
+                return qml_mtx_events::EventType::NoticeMessage;
+        }
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &)
+        {
+                return qml_mtx_events::EventType::TextMessage;
+        }
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &)
+        {
+                return qml_mtx_events::EventType::VideoMessage;
         }
-}
-
-template<class T>
-std::optional<mtx::crypto::EncryptedFile>
-eventEncryptionInfo(const mtx::events::Event<T> &)
-{
-        return std::nullopt;
-}
-
-template<class T>
-auto
-eventEncryptionInfo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<
-  std::is_same<decltype(e.content.file), std::optional<mtx::crypto::EncryptedFile>>::value,
-  std::optional<mtx::crypto::EncryptedFile>>
-{
-        return e.content.file;
-}
-
-template<class T>
-QString
-eventUrl(const mtx::events::Event<T> &)
-{
-        return "";
-}
-
-QString
-eventUrl(const mtx::events::StateEvent<mtx::events::state::Avatar> &e)
-{
-        return QString::fromStdString(e.content.url);
-}
-
-template<class T>
-auto
-eventUrl(const mtx::events::RoomEvent<T> &e)
-  -> std::enable_if_t<std::is_same<decltype(e.content.url), std::string>::value, QString>
-{
-        if (e.content.file)
-                return QString::fromStdString(e.content.file->url);
-        return QString::fromStdString(e.content.url);
-}
-
-template<class T>
-QString
-eventThumbnailUrl(const mtx::events::Event<T> &)
-{
-        return "";
-}
-template<class T>
-auto
-eventThumbnailUrl(const mtx::events::RoomEvent<T> &e)
-  -> std::enable_if_t<std::is_same<decltype(e.content.info.thumbnail_url), std::string>::value,
-                      QString>
-{
-        return QString::fromStdString(e.content.info.thumbnail_url);
-}
-
-template<class T>
-QString
-eventFilename(const mtx::events::Event<T> &)
-{
-        return "";
-}
-QString
-eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e)
-{
-        // body may be the original filename
-        return QString::fromStdString(e.content.body);
-}
-QString
-eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Video> &e)
-{
-        // body may be the original filename
-        return QString::fromStdString(e.content.body);
-}
-QString
-eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Image> &e)
-{
-        // body may be the original filename
-        return QString::fromStdString(e.content.body);
-}
-QString
-eventFilename(const mtx::events::RoomEvent<mtx::events::msg::File> &e)
-{
-        // body may be the original filename
-        if (!e.content.filename.empty())
-                return QString::fromStdString(e.content.filename);
-        return QString::fromStdString(e.content.body);
-}
-
-template<class T>
-auto
-eventFilesize(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.size)
-{
-        return e.content.info.size;
-}
-
-template<class T>
-int64_t
-eventFilesize(const mtx::events::Event<T> &)
-{
-        return 0;
-}
-
-template<class T>
-QString
-eventMimeType(const mtx::events::Event<T> &)
-{
-        return QString();
-}
-template<class T>
-auto
-eventMimeType(const mtx::events::RoomEvent<T> &e)
-  -> std::enable_if_t<std::is_same<decltype(e.content.info.mimetype), std::string>::value, QString>
-{
-        return QString::fromStdString(e.content.info.mimetype);
-}
-
-template<class T>
-QString
-eventRelatesTo(const mtx::events::Event<T> &)
-{
-        return QString();
-}
-template<class T>
-auto
-eventRelatesTo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<
-  std::is_same<decltype(e.content.relates_to.in_reply_to.event_id), std::string>::value,
-  QString>
-{
-        return QString::fromStdString(e.content.relates_to.in_reply_to.event_id);
-}
 
-template<class T>
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<T> &e)
-{
-        using mtx::events::EventType;
-        switch (e.type) {
-        case EventType::RoomKeyRequest:
-                return qml_mtx_events::EventType::KeyRequest;
-        case EventType::RoomAliases:
-                return qml_mtx_events::EventType::Aliases;
-        case EventType::RoomAvatar:
-                return qml_mtx_events::EventType::Avatar;
-        case EventType::RoomCanonicalAlias:
-                return qml_mtx_events::EventType::CanonicalAlias;
-        case EventType::RoomCreate:
-                return qml_mtx_events::EventType::Create;
-        case EventType::RoomEncrypted:
-                return qml_mtx_events::EventType::Encrypted;
-        case EventType::RoomEncryption:
-                return qml_mtx_events::EventType::Encryption;
-        case EventType::RoomGuestAccess:
-                return qml_mtx_events::EventType::GuestAccess;
-        case EventType::RoomHistoryVisibility:
-                return qml_mtx_events::EventType::HistoryVisibility;
-        case EventType::RoomJoinRules:
-                return qml_mtx_events::EventType::JoinRules;
-        case EventType::RoomMember:
-                return qml_mtx_events::EventType::Member;
-        case EventType::RoomMessage:
-                return qml_mtx_events::EventType::UnknownMessage;
-        case EventType::RoomName:
-                return qml_mtx_events::EventType::Name;
-        case EventType::RoomPowerLevels:
-                return qml_mtx_events::EventType::PowerLevels;
-        case EventType::RoomTopic:
-                return qml_mtx_events::EventType::Topic;
-        case EventType::RoomTombstone:
-                return qml_mtx_events::EventType::Tombstone;
-        case EventType::RoomRedaction:
-                return qml_mtx_events::EventType::Redaction;
-        case EventType::RoomPinnedEvents:
-                return qml_mtx_events::EventType::PinnedEvents;
-        case EventType::Sticker:
-                return qml_mtx_events::EventType::Sticker;
-        case EventType::Tag:
-                return qml_mtx_events::EventType::Tag;
-        case EventType::Unsupported:
-        default:
-                return qml_mtx_events::EventType::Unsupported;
+        qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
+        {
+                return qml_mtx_events::EventType::Redacted;
         }
-}
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::Audio> &)
-{
-        return qml_mtx_events::EventType::AudioMessage;
-}
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::Emote> &)
-{
-        return qml_mtx_events::EventType::EmoteMessage;
-}
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::File> &)
-{
-        return qml_mtx_events::EventType::FileMessage;
-}
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::Image> &)
-{
-        return qml_mtx_events::EventType::ImageMessage;
-}
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::Notice> &)
-{
-        return qml_mtx_events::EventType::NoticeMessage;
-}
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::Text> &)
-{
-        return qml_mtx_events::EventType::TextMessage;
-}
-qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::Video> &)
-{
-        return qml_mtx_events::EventType::VideoMessage;
+        // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
+        // ::EventType::LocationMessage; }
+};
 }
 
 qml_mtx_events::EventType
-toRoomEventType(const mtx::events::Event<mtx::events::msg::Redacted> &)
-{
-        return qml_mtx_events::EventType::Redacted;
-}
-// ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Location> &e) { return
-// ::EventType::LocationMessage; }
-
-template<class T>
-uint64_t
-eventHeight(const mtx::events::Event<T> &)
-{
-        return -1;
-}
-template<class T>
-auto
-eventHeight(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.h)
-{
-        return e.content.info.h;
-}
-template<class T>
-uint64_t
-eventWidth(const mtx::events::Event<T> &)
-{
-        return -1;
-}
-template<class T>
-auto
-eventWidth(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.w)
+toRoomEventType(const mtx::events::collections::TimelineEvents &event)
 {
-        return e.content.info.w;
-}
-
-template<class T>
-double
-eventPropHeight(const mtx::events::RoomEvent<T> &e)
-{
-        auto w = eventWidth(e);
-        if (w == 0)
-                w = 1;
-
-        double prop = eventHeight(e) / (double)w;
-
-        return prop > 0 ? prop : 1.;
-}
+        return std::visit(RoomEventType{}, event);
 }
 
 TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
@@ -477,6 +214,8 @@ TimelineModel::rowCount(const QModelIndex &parent) const
 QVariant
 TimelineModel::data(const QModelIndex &index, int role) const
 {
+        using namespace mtx::accessors;
+        namespace acc = mtx::accessors;
         if (index.row() < 0 && index.row() >= (int)eventOrder.size())
                 return QVariant();
 
@@ -491,86 +230,71 @@ TimelineModel::data(const QModelIndex &index, int role) const
 
         switch (role) {
         case Section: {
-                QDateTime date =
-                  std::visit([](const auto &e) -> QDateTime { return eventTimestamp(e); }, event);
+                QDateTime date = origin_server_ts(event);
                 date.setTime(QTime());
 
-                QString userId =
-                  std::visit([](const auto &e) -> QString { return senderId(e); }, event);
+                std::string userId = acc::sender(event);
 
                 for (int r = index.row() - 1; r > 0; r--) {
                         auto tempEv        = events.value(eventOrder[r]);
-                        QDateTime prevDate = std::visit(
-                          [](const auto &e) -> QDateTime { return eventTimestamp(e); }, tempEv);
+                        QDateTime prevDate = origin_server_ts(tempEv);
                         prevDate.setTime(QTime());
                         if (prevDate != date)
-                                return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId);
+                                return QString("%2 %1")
+                                  .arg(date.toMSecsSinceEpoch())
+                                  .arg(QString::fromStdString(userId));
 
-                        QString prevUserId =
-                          std::visit([](const auto &e) -> QString { return senderId(e); }, tempEv);
+                        std::string prevUserId = acc::sender(tempEv);
                         if (userId != prevUserId)
                                 break;
                 }
 
-                return QString("%1").arg(userId);
+                return QString("%1").arg(QString::fromStdString(userId));
         }
         case UserId:
-                return QVariant(
-                  std::visit([](const auto &e) -> QString { return senderId(e); }, event));
+                return QVariant(QString::fromStdString(acc::sender(event)));
         case UserName:
-                return QVariant(displayName(
-                  std::visit([](const auto &e) -> QString { return senderId(e); }, event)));
+                return QVariant(displayName(QString::fromStdString(acc::sender(event))));
 
         case Timestamp:
-                return QVariant(
-                  std::visit([](const auto &e) -> QDateTime { return eventTimestamp(e); }, event));
+                return QVariant(origin_server_ts(event));
         case Type:
-                return QVariant(std::visit(
-                  [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); },
-                  event));
+                return QVariant(toRoomEventType(event));
         case Body:
-                return QVariant(utils::replaceEmoji(
-                  std::visit([](const auto &e) -> QString { return eventBody(e); }, event)));
+                return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
         case FormattedBody:
                 return QVariant(
-                  utils::replaceEmoji(
-                    utils::linkifyMessage(std::visit(
-                      [](const auto &e) -> QString { return eventFormattedBody(e); }, event)))
+                  utils::replaceEmoji(utils::linkifyMessage(formattedBodyWithFallback(event)))
                     .remove("<mx-reply>")
                     .remove("</mx-reply>"));
         case Url:
-                return QVariant(
-                  std::visit([](const auto &e) -> QString { return eventUrl(e); }, event));
+                return QVariant(QString::fromStdString(url(event)));
         case ThumbnailUrl:
-                return QVariant(
-                  std::visit([](const auto &e) -> QString { return eventThumbnailUrl(e); }, event));
+                return QVariant(QString::fromStdString(thumbnail_url(event)));
         case Filename:
-                return QVariant(
-                  std::visit([](const auto &e) -> QString { return eventFilename(e); }, event));
+                return QVariant(QString::fromStdString(filename(event)));
         case Filesize:
-                return QVariant(std::visit(
-                  [](const auto &e) -> QString {
-                          return utils::humanReadableFileSize(eventFilesize(e));
-                  },
-                  event));
+                return QVariant(utils::humanReadableFileSize(filesize(event)));
         case MimeType:
-                return QVariant(
-                  std::visit([](const auto &e) -> QString { return eventMimeType(e); }, event));
+                return QVariant(QString::fromStdString(mimetype(event)));
         case Height:
-                return QVariant(
-                  std::visit([](const auto &e) -> qulonglong { return eventHeight(e); }, event));
+                return QVariant(qulonglong{media_height(event)});
         case Width:
-                return QVariant(
-                  std::visit([](const auto &e) -> qulonglong { return eventWidth(e); }, event));
-        case ProportionalHeight:
-                return QVariant(
-                  std::visit([](const auto &e) -> double { return eventPropHeight(e); }, event));
+                return QVariant(qulonglong{media_width(event)});
+        case ProportionalHeight: {
+                auto w = media_width(event);
+                if (w == 0)
+                        w = 1;
+
+                double prop = media_height(event) / (double)w;
+
+                return QVariant(prop > 0 ? prop : 1.);
+        }
         case Id:
                 return id;
         case State:
                 // only show read receipts for messages not from us
-                if (std::visit([](const auto &e) -> QString { return senderId(e); }, event)
-                      .toStdString() != http::client()->user_id().to_string())
+                if (acc::sender(event) != http::client()->user_id().to_string())
                         return qml_mtx_events::Empty;
                 else if (failed.contains(id))
                         return qml_mtx_events::Failed;
@@ -581,21 +305,15 @@ TimelineModel::data(const QModelIndex &index, int role) const
                 else
                         return qml_mtx_events::Received;
         case IsEncrypted: {
-                auto tempEvent = events[id];
                 return std::holds_alternative<
-                  mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(tempEvent);
-        }
-        case ReplyTo: {
-                QString evId =
-                  std::visit([](const auto &e) -> QString { return eventRelatesTo(e); }, event);
-                return QVariant(evId);
+                  mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]);
         }
+        case ReplyTo:
+                return QVariant(QString::fromStdString(in_reply_to_event(event)));
         case RoomName:
-                return QVariant(
-                  std::visit([](const auto &e) -> QString { return eventRoomName(e); }, event));
+                return QVariant(QString::fromStdString(room_name(event)));
         case RoomTopic:
-                return QVariant(
-                  std::visit([](const auto &e) -> QString { return eventRoomTopic(e); }, event));
+                return QVariant(QString::fromStdString(room_topic(event)));
         default:
                 return QVariant();
         }
@@ -667,7 +385,7 @@ TimelineModel::internalAddEvents(
 {
         std::vector<QString> ids;
         for (auto e : timeline) {
-                QString id = std::visit([](const auto &e) -> QString { return eventId(e); }, e);
+                QString id = QString::fromStdString(mtx::accessors::event_id(e));
 
                 if (this->events.contains(id)) {
                         this->events.insert(id, e);
@@ -708,11 +426,7 @@ TimelineModel::internalAddEvents(
                       std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
                         e = decryptEvent(*event).event;
                 }
-                auto encInfo = std::visit(
-                  [](const auto &ev) -> std::optional<mtx::crypto::EncryptedFile> {
-                          return eventEncryptionInfo(ev);
-                  },
-                  e);
+                auto encInfo = mtx::accessors::file(e);
 
                 if (encInfo)
                         emit newEncryptedImage(encInfo.value());
@@ -950,25 +664,18 @@ TimelineModel::replyAction(QString id)
                 event = decryptEvent(*e).event;
         }
 
-        RelatedInfo related = std::visit(
-          [](const auto &ev) -> RelatedInfo {
-                  RelatedInfo related_   = {};
-                  related_.quoted_user   = QString::fromStdString(ev.sender);
-                  related_.related_event = ev.event_id;
-                  return related_;
-          },
-          event);
-        related.type = mtx::events::getMessageType(
-          std::visit([](const auto &e) -> std::string { return eventMsgType(e); }, event));
-        related.quoted_body =
-          std::visit([](const auto &e) -> QString { return eventFormattedBody(e); }, event);
+        RelatedInfo related   = {};
+        related.quoted_user   = QString::fromStdString(mtx::accessors::sender(event));
+        related.related_event = mtx::accessors::event_id(event);
+        related.type          = mtx::accessors::msg_type(event);
+        related.quoted_body   = mtx::accessors::formattedBodyWithFallback(event);
         related.quoted_body.remove(QRegularExpression(
           "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
         nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString());
         related.room = room_id_;
 
-        if (related.quoted_body.isEmpty())
-                return;
+        // if (related.quoted_body.isEmpty())
+        //        return;
 
         ChatPage::instance()->messageReply(related);
 }
@@ -1412,8 +1119,7 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
 
         internalAddEvents({event});
 
-        QString txn_id_qstr =
-          std::visit([](const auto &e) -> QString { return eventId(e); }, event);
+        QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event));
         beginInsertRows(QModelIndex(),
                         static_cast<int>(this->eventOrder.size()),
                         static_cast<int>(this->eventOrder.size()));
@@ -1436,18 +1142,13 @@ TimelineModel::saveMedia(QString eventId) const
                 event = decryptEvent(*e).event;
         }
 
-        QString mxcUrl = std::visit([](const auto &e) -> QString { return eventUrl(e); }, event);
-        QString originalFilename =
-          std::visit([](const auto &e) -> QString { return eventFilename(e); }, event);
-        QString mimeType =
-          std::visit([](const auto &e) -> QString { return eventMimeType(e); }, event);
+        QString mxcUrl           = QString::fromStdString(mtx::accessors::url(event));
+        QString originalFilename = QString::fromStdString(mtx::accessors::filename(event));
+        QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(event));
 
-        using EncF = std::optional<mtx::crypto::EncryptedFile>;
-        EncF encryptionInfo =
-          std::visit([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event);
+        auto encryptionInfo = mtx::accessors::file(event);
 
-        qml_mtx_events::EventType eventType = std::visit(
-          [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event);
+        qml_mtx_events::EventType eventType = toRoomEventType(event);
 
         QString dialogTitle;
         if (eventType == qml_mtx_events::EventType::ImageMessage) {
@@ -1513,13 +1214,11 @@ TimelineModel::cacheMedia(QString eventId)
                 event = decryptEvent(*e).event;
         }
 
-        QString mxcUrl = std::visit([](const auto &e) -> QString { return eventUrl(e); }, event);
-        QString mimeType =
-          std::visit([](const auto &e) -> QString { return eventMimeType(e); }, event);
+        QString mxcUrl           = QString::fromStdString(mtx::accessors::url(event));
+        QString originalFilename = QString::fromStdString(mtx::accessors::filename(event));
+        QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(event));
 
-        using EncF = std::optional<mtx::crypto::EncryptedFile>;
-        EncF encryptionInfo =
-          std::visit([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event);
+        auto encryptionInfo = mtx::accessors::file(event);
 
         // If the message is a link to a non mxcUrl, don't download it
         if (!mxcUrl.startsWith("mxc://")) {