diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
new file mode 100644
index 00000000..6264af58
--- /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);
+}
|