summary refs log tree commit diff
path: root/src/timeline
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2019-11-09 03:06:10 +0100
committerNicolas Werner <nicolas.werner@hotmail.de>2019-11-23 20:07:15 +0100
commit91d1f19058a31cc35ca1212f042a9dd6f501a7b7 (patch)
treec37888c720cce2d1736afbd071cf9dd75e1a9391 /src/timeline
parentUse default macOS image (diff)
downloadnheko-91d1f19058a31cc35ca1212f042a9dd6f501a7b7.tar.xz
Remove old timeline
Diffstat (limited to 'src/timeline')
-rw-r--r--src/timeline/DelegateChooser.cpp138
-rw-r--r--src/timeline/DelegateChooser.h82
-rw-r--r--src/timeline/TimelineItem.cpp960
-rw-r--r--src/timeline/TimelineItem.h389
-rw-r--r--src/timeline/TimelineModel.cpp1220
-rw-r--r--src/timeline/TimelineModel.h258
-rw-r--r--src/timeline/TimelineView.cpp1627
-rw-r--r--src/timeline/TimelineView.h449
-rw-r--r--src/timeline/TimelineViewManager.cpp564
-rw-r--r--src/timeline/TimelineViewManager.h111
-rw-r--r--src/timeline/widgets/AudioItem.cpp236
-rw-r--r--src/timeline/widgets/AudioItem.h104
-rw-r--r--src/timeline/widgets/FileItem.cpp221
-rw-r--r--src/timeline/widgets/FileItem.h79
-rw-r--r--src/timeline/widgets/ImageItem.cpp267
-rw-r--r--src/timeline/widgets/ImageItem.h104
-rw-r--r--src/timeline/widgets/VideoItem.cpp65
-rw-r--r--src/timeline/widgets/VideoItem.h51
18 files changed, 2075 insertions, 4850 deletions
diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp
new file mode 100644
index 00000000..632a2a64
--- /dev/null
+++ b/src/timeline/DelegateChooser.cpp
@@ -0,0 +1,138 @@
+#include "DelegateChooser.h"
+
+#include "Logging.h"
+
+// uses private API, which moved between versions
+#include <QQmlEngine>
+#include <QtGlobal>
+
+QQmlComponent *
+DelegateChoice::delegate() const
+{
+        return delegate_;
+}
+
+void
+DelegateChoice::setDelegate(QQmlComponent *delegate)
+{
+        if (delegate != delegate_) {
+                delegate_ = delegate;
+                emit delegateChanged();
+                emit changed();
+        }
+}
+
+QVariant
+DelegateChoice::roleValue() const
+{
+        return roleValue_;
+}
+
+void
+DelegateChoice::setRoleValue(const QVariant &value)
+{
+        if (value != roleValue_) {
+                roleValue_ = value;
+                emit roleValueChanged();
+                emit changed();
+        }
+}
+
+QVariant
+DelegateChooser::roleValue() const
+{
+        return roleValue_;
+}
+
+void
+DelegateChooser::setRoleValue(const QVariant &value)
+{
+        if (value != roleValue_) {
+                roleValue_ = value;
+                recalcChild();
+                emit roleValueChanged();
+        }
+}
+
+QQmlListProperty<DelegateChoice>
+DelegateChooser::choices()
+{
+        return QQmlListProperty<DelegateChoice>(this,
+                                                this,
+                                                &DelegateChooser::appendChoice,
+                                                &DelegateChooser::choiceCount,
+                                                &DelegateChooser::choice,
+                                                &DelegateChooser::clearChoices);
+}
+
+void
+DelegateChooser::appendChoice(QQmlListProperty<DelegateChoice> *p, DelegateChoice *c)
+{
+        DelegateChooser *dc = static_cast<DelegateChooser *>(p->object);
+        dc->choices_.append(c);
+}
+
+int
+DelegateChooser::choiceCount(QQmlListProperty<DelegateChoice> *p)
+{
+        return static_cast<DelegateChooser *>(p->object)->choices_.count();
+}
+DelegateChoice *
+DelegateChooser::choice(QQmlListProperty<DelegateChoice> *p, int index)
+{
+        return static_cast<DelegateChooser *>(p->object)->choices_.at(index);
+}
+void
+DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p)
+{
+        static_cast<DelegateChooser *>(p->object)->choices_.clear();
+}
+
+void
+DelegateChooser::recalcChild()
+{
+        for (const auto choice : choices_) {
+                auto choiceValue = choice->roleValue();
+                if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) {
+                        if (child) {
+                                child->setParentItem(nullptr);
+                                child = nullptr;
+                        }
+
+                        choice->delegate()->create(incubator, QQmlEngine::contextForObject(this));
+                        return;
+                }
+        }
+}
+
+void
+DelegateChooser::componentComplete()
+{
+        QQuickItem::componentComplete();
+        recalcChild();
+}
+
+void
+DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status)
+{
+        if (status == QQmlIncubator::Ready) {
+                chooser.child = dynamic_cast<QQuickItem *>(object());
+                if (chooser.child == nullptr) {
+                        nhlog::ui()->error("Delegate has to be derived of Item!");
+                        return;
+                }
+
+                chooser.child->setParentItem(&chooser);
+                connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() {
+                        chooser.setHeight(chooser.child->height());
+                });
+                chooser.setHeight(chooser.child->height());
+                QQmlEngine::setObjectOwnership(chooser.child,
+                                               QQmlEngine::ObjectOwnership::JavaScriptOwnership);
+
+        } else if (status == QQmlIncubator::Error) {
+                for (const auto &e : errors())
+                        nhlog::ui()->error("Error instantiating delegate: {}",
+                                           e.toString().toStdString());
+        }
+}
diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h
new file mode 100644
index 00000000..68ebeb04
--- /dev/null
+++ b/src/timeline/DelegateChooser.h
@@ -0,0 +1,82 @@
+// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt
+// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent
+
+#pragma once
+
+#include <QQmlComponent>
+#include <QQmlIncubator>
+#include <QQmlListProperty>
+#include <QQuickItem>
+#include <QtCore/QObject>
+#include <QtCore/QVariant>
+
+class QQmlAdaptorModel;
+
+class DelegateChoice : public QObject
+{
+        Q_OBJECT
+        Q_CLASSINFO("DefaultProperty", "delegate")
+
+public:
+        Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged)
+        Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
+
+        QQmlComponent *delegate() const;
+        void setDelegate(QQmlComponent *delegate);
+
+        QVariant roleValue() const;
+        void setRoleValue(const QVariant &value);
+
+signals:
+        void delegateChanged();
+        void roleValueChanged();
+        void changed();
+
+private:
+        QVariant roleValue_;
+        QQmlComponent *delegate_ = nullptr;
+};
+
+class DelegateChooser : public QQuickItem
+{
+        Q_OBJECT
+        Q_CLASSINFO("DefaultProperty", "choices")
+
+public:
+        Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT)
+        Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged)
+
+        QQmlListProperty<DelegateChoice> choices();
+
+        QVariant roleValue() const;
+        void setRoleValue(const QVariant &value);
+
+        void recalcChild();
+        void componentComplete() override;
+
+signals:
+        void roleChanged();
+        void roleValueChanged();
+
+private:
+        struct DelegateIncubator : public QQmlIncubator
+        {
+                DelegateIncubator(DelegateChooser &parent)
+                  : QQmlIncubator(QQmlIncubator::AsynchronousIfNested)
+                  , chooser(parent)
+                {}
+                void statusChanged(QQmlIncubator::Status status) override;
+
+                DelegateChooser &chooser;
+        };
+
+        QVariant roleValue_;
+        QList<DelegateChoice *> choices_;
+        QQuickItem *child = nullptr;
+        DelegateIncubator incubator{*this};
+
+        static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *);
+        static int choiceCount(QQmlListProperty<DelegateChoice> *);
+        static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index);
+        static void clearChoices(QQmlListProperty<DelegateChoice> *);
+};
diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp
deleted file mode 100644
index 7916bd80..00000000
--- a/src/timeline/TimelineItem.cpp
+++ /dev/null
@@ -1,960 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-#include <functional>
-
-#include <QContextMenuEvent>
-#include <QDesktopServices>
-#include <QFontDatabase>
-#include <QMenu>
-#include <QTimer>
-#include <QtGlobal>
-
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "MainWindow.h"
-#include "Olm.h"
-#include "ui/Avatar.h"
-#include "ui/Painter.h"
-#include "ui/TextLabel.h"
-
-#include "timeline/TimelineItem.h"
-#include "timeline/widgets/AudioItem.h"
-#include "timeline/widgets/FileItem.h"
-#include "timeline/widgets/ImageItem.h"
-#include "timeline/widgets/VideoItem.h"
-
-#include "dialogs/RawMessage.h"
-#include "mtx/identifiers.hpp"
-
-constexpr int MSG_RIGHT_MARGIN = 7;
-constexpr int MSG_PADDING      = 20;
-
-StatusIndicator::StatusIndicator(QWidget *parent)
-  : QWidget(parent)
-{
-        lockIcon_.addFile(":/icons/icons/ui/lock.png");
-        clockIcon_.addFile(":/icons/icons/ui/clock.png");
-        checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png");
-        doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png");
-}
-
-void
-StatusIndicator::paintIcon(QPainter &p, QIcon &icon)
-{
-        auto pixmap = icon.pixmap(width());
-
-        QPainter painter(&pixmap);
-        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
-        painter.fillRect(pixmap.rect(), p.pen().color());
-
-        QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal);
-}
-
-void
-StatusIndicator::paintEvent(QPaintEvent *)
-{
-        if (state_ == StatusIndicatorState::Empty)
-                return;
-
-        Painter p(this);
-        PainterHighQualityEnabler hq(p);
-
-        p.setPen(iconColor_);
-
-        switch (state_) {
-        case StatusIndicatorState::Sent: {
-                paintIcon(p, clockIcon_);
-                break;
-        }
-        case StatusIndicatorState::Encrypted:
-                paintIcon(p, lockIcon_);
-                break;
-        case StatusIndicatorState::Received: {
-                paintIcon(p, checkmarkIcon_);
-                break;
-        }
-        case StatusIndicatorState::Read: {
-                paintIcon(p, doubleCheckmarkIcon_);
-                break;
-        }
-        case StatusIndicatorState::Empty:
-                break;
-        }
-}
-
-void
-StatusIndicator::setState(StatusIndicatorState state)
-{
-        state_ = state;
-
-        switch (state) {
-        case StatusIndicatorState::Encrypted:
-                setToolTip(tr("Encrypted"));
-                break;
-        case StatusIndicatorState::Received:
-                setToolTip(tr("Delivered"));
-                break;
-        case StatusIndicatorState::Read:
-                setToolTip(tr("Seen"));
-                break;
-        case StatusIndicatorState::Sent:
-                setToolTip(tr("Sent"));
-                break;
-        case StatusIndicatorState::Empty:
-                setToolTip("");
-                break;
-        }
-
-        update();
-}
-
-void
-TimelineItem::adjustMessageLayoutForWidget()
-{
-        messageLayout_->addLayout(widgetLayout_, 1);
-        actionLayout_->addWidget(replyBtn_);
-        actionLayout_->addWidget(contextBtn_);
-        messageLayout_->addLayout(actionLayout_);
-        messageLayout_->addWidget(statusIndicator_);
-        messageLayout_->addWidget(timestamp_);
-
-        actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);
-        actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);
-        messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);
-        messageLayout_->setAlignment(timestamp_, Qt::AlignTop);
-        messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);
-
-        mainLayout_->addLayout(messageLayout_);
-}
-
-void
-TimelineItem::adjustMessageLayout()
-{
-        messageLayout_->addWidget(body_, 1);
-        actionLayout_->addWidget(replyBtn_);
-        actionLayout_->addWidget(contextBtn_);
-        messageLayout_->addLayout(actionLayout_);
-        messageLayout_->addWidget(statusIndicator_);
-        messageLayout_->addWidget(timestamp_);
-
-        actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);
-        actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);
-        messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);
-        messageLayout_->setAlignment(timestamp_, Qt::AlignTop);
-        messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);
-
-        mainLayout_->addLayout(messageLayout_);
-}
-
-void
-TimelineItem::init()
-{
-        userAvatar_      = nullptr;
-        timestamp_       = nullptr;
-        userName_        = nullptr;
-        body_            = nullptr;
-        auto buttonSize_ = 32;
-
-        contextMenu_      = new QMenu(this);
-        showReadReceipts_ = new QAction("Read receipts", this);
-        markAsRead_       = new QAction("Mark as read", this);
-        viewRawMessage_   = new QAction("View raw message", this);
-        redactMsg_        = new QAction("Redact message", this);
-        contextMenu_->addAction(showReadReceipts_);
-        contextMenu_->addAction(viewRawMessage_);
-        contextMenu_->addAction(markAsRead_);
-        contextMenu_->addAction(redactMsg_);
-
-        connect(showReadReceipts_, &QAction::triggered, this, [this]() {
-                if (!event_id_.isEmpty())
-                        MainWindow::instance()->openReadReceiptsDialog(event_id_);
-        });
-
-        connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {
-                emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);
-        });
-        connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {
-                emit ChatPage::instance()->showNotification(msg);
-        });
-        connect(redactMsg_, &QAction::triggered, this, [this]() {
-                if (!event_id_.isEmpty())
-                        http::client()->redact_event(
-                          room_id_.toStdString(),
-                          event_id_.toStdString(),
-                          [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
-                                  if (err) {
-                                          emit redactionFailed(tr("Message redaction failed: %1")
-                                                                 .arg(QString::fromStdString(
-                                                                   err->matrix_error.error)));
-                                          return;
-                                  }
-
-                                  emit eventRedacted(event_id_);
-                          });
-        });
-        connect(
-          ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor);
-        connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt);
-        connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer);
-
-        colorGenerating_ = new QFutureWatcher<QString>(this);
-        connect(colorGenerating_,
-                &QFutureWatcher<QString>::finished,
-                this,
-                &TimelineItem::finishedGeneratingColor);
-
-        topLayout_     = new QHBoxLayout(this);
-        mainLayout_    = new QVBoxLayout;
-        messageLayout_ = new QHBoxLayout;
-        actionLayout_  = new QHBoxLayout;
-        messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0);
-        messageLayout_->setSpacing(MSG_PADDING);
-
-        actionLayout_->setContentsMargins(13, 1, 13, 0);
-        actionLayout_->setSpacing(0);
-
-        topLayout_->setContentsMargins(
-          conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0);
-        topLayout_->setSpacing(0);
-        topLayout_->addLayout(mainLayout_);
-
-        mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);
-        mainLayout_->setSpacing(0);
-
-        replyBtn_ = new FlatButton(this);
-        replyBtn_->setToolTip(tr("Reply"));
-        replyBtn_->setFixedSize(buttonSize_, buttonSize_);
-        replyBtn_->setCornerRadius(buttonSize_ / 2);
-
-        QIcon reply_icon;
-        reply_icon.addFile(":/icons/icons/ui/mail-reply.png");
-        replyBtn_->setIcon(reply_icon);
-        replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
-        connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction);
-
-        contextBtn_ = new FlatButton(this);
-        contextBtn_->setToolTip(tr("Options"));
-        contextBtn_->setFixedSize(buttonSize_, buttonSize_);
-        contextBtn_->setCornerRadius(buttonSize_ / 2);
-
-        QIcon context_icon;
-        context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");
-        contextBtn_->setIcon(context_icon);
-        contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
-        contextBtn_->setMenu(contextMenu_);
-
-        timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);
-        timestampFont_.setFamily("Monospace");
-        timestampFont_.setStyleHint(QFont::Monospace);
-
-        QFontMetrics tsFm(timestampFont_);
-
-        statusIndicator_ = new StatusIndicator(this);
-        statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());
-        statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());
-
-        parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-}
-
-/*
- * For messages created locally.
- */
-TimelineItem::TimelineItem(mtx::events::MessageType ty,
-                           const QString &userid,
-                           QString body,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(ty)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        auto displayName = Cache::displayName(room_id_, userid);
-        auto timestamp   = QDateTime::currentDateTime();
-
-        // Generate the html body to be rendered.
-        auto formatted_body = utils::markdownToHtml(body);
-
-        // Escape html if the input is not formatted.
-        if (formatted_body == body.trimmed().toHtmlEscaped())
-                formatted_body = body.toHtmlEscaped();
-
-        QString emptyEventId;
-
-        if (ty == mtx::events::MessageType::Emote) {
-                formatted_body  = QString("<em>%1</em>").arg(formatted_body);
-                descriptionMsg_ = {emptyEventId,
-                                   "",
-                                   userid,
-                                   QString("* %1 %2").arg(displayName).arg(body),
-                                   utils::descriptiveTime(timestamp),
-                                   timestamp};
-        } else {
-                descriptionMsg_ = {emptyEventId,
-                                   "You: ",
-                                   userid,
-                                   body,
-                                   utils::descriptiveTime(timestamp),
-                                   timestamp};
-        }
-
-        formatted_body = utils::linkifyMessage(formatted_body);
-        formatted_body.replace("mx-reply", "div");
-
-        generateTimestamp(timestamp);
-
-        if (withSender) {
-                generateBody(userid, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(userid);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-TimelineItem::TimelineItem(ImageItem *image,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::Image)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<ImageItem>(image, userid, withSender);
-
-        addSaveImageAction(image);
-}
-
-TimelineItem::TimelineItem(FileItem *file,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::File)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<FileItem>(file, userid, withSender);
-}
-
-TimelineItem::TimelineItem(AudioItem *audio,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::Audio)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<AudioItem>(audio, userid, withSender);
-}
-
-TimelineItem::TimelineItem(VideoItem *video,
-                           const QString &userid,
-                           bool withSender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget{parent}
-  , message_type_(mtx::events::MessageType::Video)
-  , room_id_{room_id}
-{
-        init();
-
-        setupLocalWidgetLayout<VideoItem>(video, userid, withSender);
-}
-
-TimelineItem::TimelineItem(ImageItem *image,
-                           const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Image)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(
-          image, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-
-        addSaveImageAction(image);
-}
-
-TimelineItem::TimelineItem(StickerItem *image,
-                           const mtx::events::Sticker &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-
-        addSaveImageAction(image);
-}
-
-TimelineItem::TimelineItem(FileItem *file,
-                           const mtx::events::RoomEvent<mtx::events::msg::File> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::File)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(
-          file, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-}
-
-TimelineItem::TimelineItem(AudioItem *audio,
-                           const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Audio)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(
-          audio, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-}
-
-TimelineItem::TimelineItem(VideoItem *video,
-                           const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Video)
-  , room_id_{room_id}
-{
-        setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(
-          video, event, with_sender);
-
-        markOwnMessagesAsReceived(event.sender);
-}
-
-/*
- * Used to display remote notice messages.
- */
-TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Notice)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        markOwnMessagesAsReceived(event.sender);
-
-        event_id_            = QString::fromStdString(event.event_id);
-        const auto sender    = QString::fromStdString(event.sender);
-        const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-
-        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
-        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
-
-        descriptionMsg_ = {event_id_,
-                           Cache::displayName(room_id_, sender),
-                           sender,
-                           " sent a notification",
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        if (with_sender) {
-                auto displayName = Cache::displayName(room_id_, sender);
-
-                generateBody(sender, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-/*
- * Used to display remote emote messages.
- */
-TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Emote)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        markOwnMessagesAsReceived(event.sender);
-
-        event_id_         = QString::fromStdString(event.event_id);
-        const auto sender = QString::fromStdString(event.sender);
-
-        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
-        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
-
-        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-        auto displayName = Cache::displayName(room_id_, sender);
-        formatted_body   = QString("<em>%1</em>").arg(formatted_body);
-
-        descriptionMsg_ = {event_id_,
-                           "",
-                           sender,
-                           QString("* %1 %2").arg(displayName).arg(body),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        if (with_sender) {
-                generateBody(sender, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-/*
- * Used to display remote text messages.
- */
-TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event,
-                           bool with_sender,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , message_type_(mtx::events::MessageType::Text)
-  , room_id_{room_id}
-{
-        init();
-        addReplyAction();
-
-        markOwnMessagesAsReceived(event.sender);
-
-        event_id_         = QString::fromStdString(event.event_id);
-        const auto sender = QString::fromStdString(event.sender);
-
-        auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
-        auto body           = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
-
-        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-        auto displayName = Cache::displayName(room_id_, sender);
-
-        QSettings settings;
-        descriptionMsg_ = {event_id_,
-                           sender == settings.value("auth/user_id") ? "You" : displayName,
-                           sender,
-                           QString(": %1").arg(body),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        if (with_sender) {
-                generateBody(sender, displayName, formatted_body);
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                generateBody(formatted_body);
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayout();
-}
-
-TimelineItem::~TimelineItem()
-{
-        colorGenerating_->cancel();
-        colorGenerating_->waitForFinished();
-}
-
-void
-TimelineItem::markSent()
-{
-        statusIndicator_->setState(StatusIndicatorState::Sent);
-}
-
-void
-TimelineItem::markOwnMessagesAsReceived(const std::string &sender)
-{
-        QSettings settings;
-        if (sender == settings.value("auth/user_id").toString().toStdString())
-                statusIndicator_->setState(StatusIndicatorState::Received);
-}
-
-void
-TimelineItem::markRead()
-{
-        if (statusIndicator_->state() != StatusIndicatorState::Encrypted)
-                statusIndicator_->setState(StatusIndicatorState::Read);
-}
-
-void
-TimelineItem::markReceived(bool isEncrypted)
-{
-        isReceived_ = true;
-
-        if (isEncrypted)
-                statusIndicator_->setState(StatusIndicatorState::Encrypted);
-        else
-                statusIndicator_->setState(StatusIndicatorState::Received);
-
-        sendReadReceipt();
-}
-
-// Only the body is displayed.
-void
-TimelineItem::generateBody(const QString &body)
-{
-        body_ = new TextLabel(utils::replaceEmoji(body), this);
-        body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
-
-        connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {
-                MainWindow::instance()->openUserProfile(user_id,
-                                                        ChatPage::instance()->currentRoom());
-        });
-}
-
-void
-TimelineItem::refreshAuthorColor()
-{
-        // Cancel and wait if we are already generating the color.
-        if (colorGenerating_->isRunning()) {
-                colorGenerating_->cancel();
-                colorGenerating_->waitForFinished();
-        }
-        if (userName_) {
-                // generate user's unique color.
-                std::function<QString()> generate = [this]() {
-                        QString userColor = utils::generateContrastingHexColor(
-                          userName_->toolTip(), backgroundColor().name());
-                        return userColor;
-                };
-
-                QString userColor = Cache::userColor(userName_->toolTip());
-
-                // If the color is empty, then generate it asynchronously
-                if (userColor.isEmpty()) {
-                        colorGenerating_->setFuture(QtConcurrent::run(generate));
-                } else {
-                        userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
-                }
-        }
-}
-
-void
-TimelineItem::finishedGeneratingColor()
-{
-        nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString());
-        QString userColor = colorGenerating_->result();
-
-        if (!userColor.isEmpty()) {
-                // another TimelineItem might have inserted in the meantime.
-                if (Cache::userColor(userName_->toolTip()).isEmpty()) {
-                        Cache::insertUserColor(userName_->toolTip(), userColor);
-                }
-                userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
-        }
-}
-// The username/timestamp is displayed along with the message body.
-void
-TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body)
-{
-        generateUserName(user_id, displayname);
-        generateBody(body);
-}
-
-void
-TimelineItem::generateUserName(const QString &user_id, const QString &displayname)
-{
-        auto sender = displayname;
-
-        if (displayname.startsWith("@")) {
-                // TODO: Fix this by using a UserId type.
-                if (displayname.split(":")[0].split("@").size() > 1)
-                        sender = displayname.split(":")[0].split("@")[1];
-        }
-
-        QFont usernameFont;
-        usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1);
-        usernameFont.setWeight(QFont::Medium);
-
-        QFontMetrics fm(usernameFont);
-
-        userName_ = new QLabel(this);
-        userName_->setFont(usernameFont);
-        userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500)));
-        userName_->setToolTip(user_id);
-        userName_->setToolTipDuration(1500);
-        userName_->setAttribute(Qt::WA_Hover);
-        userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-        // width deprecated in 5.13:
-        userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text()));
-#else
-        userName_->setFixedWidth(
-          QFontMetrics(userName_->font()).horizontalAdvance(userName_->text()));
-#endif
-        // Set the user color asynchronously if it hasn't been generated yet,
-        // otherwise this will just set it.
-        refreshAuthorColor();
-
-        auto filter = new UserProfileFilter(user_id, userName_);
-        userName_->installEventFilter(filter);
-        userName_->setCursor(Qt::PointingHandCursor);
-
-        connect(filter, &UserProfileFilter::hoverOn, this, [this]() {
-                QFont f = userName_->font();
-                f.setUnderline(true);
-                userName_->setFont(f);
-        });
-
-        connect(filter, &UserProfileFilter::hoverOff, this, [this]() {
-                QFont f = userName_->font();
-                f.setUnderline(false);
-                userName_->setFont(f);
-        });
-
-        connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() {
-                MainWindow::instance()->openUserProfile(user_id, room_id_);
-        });
-}
-
-void
-TimelineItem::generateTimestamp(const QDateTime &time)
-{
-        timestamp_ = new QLabel(this);
-        timestamp_->setFont(timestampFont_);
-        timestamp_->setText(
-          QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));
-}
-
-void
-TimelineItem::setupAvatarLayout(const QString &userName)
-{
-        topLayout_->setContentsMargins(
-          conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0);
-
-        QFont f;
-        f.setPointSizeF(f.pointSizeF());
-
-        userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2);
-        userAvatar_->setLetter(QChar(userName[0]).toUpper());
-
-        // TODO: The provided user name should be a UserId class
-        if (userName[0] == '@' && userName.size() > 1)
-                userAvatar_->setLetter(QChar(userName[1]).toUpper());
-
-        topLayout_->insertWidget(0, userAvatar_);
-        topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft);
-
-        if (userName_)
-                mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft);
-}
-
-void
-TimelineItem::setupSimpleLayout()
-{
-        QFont f;
-        f.setPointSizeF(f.pointSizeF());
-
-        topLayout_->setContentsMargins(conf::timeline::msgLeftMargin +
-                                         QFontMetrics(f).height() * 2 + 2,
-                                       conf::timeline::msgTopMargin,
-                                       0,
-                                       0);
-}
-
-void
-TimelineItem::setUserAvatar(const QString &userid)
-{
-        if (userAvatar_ == nullptr)
-                return;
-
-        userAvatar_->setImage(room_id_, userid);
-}
-
-void
-TimelineItem::contextMenuEvent(QContextMenuEvent *event)
-{
-        if (contextMenu_)
-                contextMenu_->exec(event->globalPos());
-}
-
-void
-TimelineItem::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-TimelineItem::addSaveImageAction(ImageItem *image)
-{
-        if (contextMenu_) {
-                auto saveImage = new QAction("Save image", this);
-                contextMenu_->addAction(saveImage);
-
-                connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs);
-        }
-}
-
-void
-TimelineItem::addReplyAction()
-{
-        if (contextMenu_) {
-                auto replyAction = new QAction("Reply", this);
-                contextMenu_->addAction(replyAction);
-
-                connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction);
-        }
-}
-
-void
-TimelineItem::replyAction()
-{
-        if (!body_)
-                return;
-
-        RelatedInfo related;
-        related.type          = message_type_;
-        related.quoted_body   = body_->toPlainText();
-        related.quoted_user   = descriptionMsg_.userid;
-        related.related_event = eventId().toStdString();
-        related.room          = room_id_;
-
-        emit ChatPage::instance()->messageReply(related);
-}
-
-void
-TimelineItem::addKeyRequestAction()
-{
-        if (contextMenu_) {
-                auto requestKeys = new QAction("Request encryption keys", this);
-                contextMenu_->addAction(requestKeys);
-
-                connect(requestKeys, &QAction::triggered, this, [this]() {
-                        olm::request_keys(room_id_.toStdString(), event_id_.toStdString());
-                });
-        }
-}
-
-void
-TimelineItem::addAvatar()
-{
-        if (userAvatar_)
-                return;
-
-        // TODO: should be replaced with the proper event struct.
-        auto userid      = descriptionMsg_.userid;
-        auto displayName = Cache::displayName(room_id_, userid);
-
-        generateUserName(userid, displayName);
-
-        setupAvatarLayout(displayName);
-
-        setUserAvatar(userid);
-}
-
-void
-TimelineItem::sendReadReceipt() const
-{
-        if (!event_id_.isEmpty())
-                http::client()->read_event(room_id_.toStdString(),
-                                           event_id_.toStdString(),
-                                           [this](mtx::http::RequestErr err) {
-                                                   if (err) {
-                                                           nhlog::net()->warn(
-                                                             "failed to read_event ({}, {})",
-                                                             room_id_.toStdString(),
-                                                             event_id_.toStdString());
-                                                   }
-                                           });
-}
-
-void
-TimelineItem::openRawMessageViewer() const
-{
-        const auto event_id = event_id_.toStdString();
-        const auto room_id  = room_id_.toStdString();
-
-        auto proxy = std::make_shared<EventProxy>();
-        connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) {
-                auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))};
-                Q_UNUSED(dialog);
-        });
-
-        http::client()->get_event(
-          room_id,
-          event_id,
-          [event_id, room_id, proxy = std::move(proxy)](
-            const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) {
-                  using namespace mtx::events;
-
-                  if (err) {
-                          nhlog::net()->warn(
-                            "failed to retrieve event {} from {}", event_id, room_id);
-                          return;
-                  }
-
-                  try {
-                          emit proxy->eventRetrieved(utils::serialize_event(res));
-                  } catch (const nlohmann::json::exception &e) {
-                          nhlog::net()->warn(
-                            "failed to serialize event ({}, {})", room_id, event_id);
-                  }
-          });
-}
diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h
deleted file mode 100644
index 356976e5..00000000
--- a/src/timeline/TimelineItem.h
+++ /dev/null
@@ -1,389 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QApplication>
-#include <QDateTime>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QLayout>
-#include <QPainter>
-#include <QSettings>
-#include <QTimer>
-
-#include <QtConcurrent>
-
-#include "mtx/events.hpp"
-
-#include "AvatarProvider.h"
-#include "RoomInfoListItem.h"
-#include "Utils.h"
-
-#include "Cache.h"
-#include "MatrixClient.h"
-
-#include "ui/FlatButton.h"
-
-class ImageItem;
-class StickerItem;
-class AudioItem;
-class VideoItem;
-class FileItem;
-class Avatar;
-class TextLabel;
-
-enum class StatusIndicatorState
-{
-        //! The encrypted message was received by the server.
-        Encrypted,
-        //! The plaintext message was received by the server.
-        Received,
-        //! At least one of the participants has read the message.
-        Read,
-        //! The client sent the message. Not yet received.
-        Sent,
-        //! When the message is loaded from cache or backfill.
-        Empty,
-};
-
-//!
-//! Used to notify the user about the status of a message.
-//!
-class StatusIndicator : public QWidget
-{
-        Q_OBJECT
-
-public:
-        explicit StatusIndicator(QWidget *parent);
-        void setState(StatusIndicatorState state);
-        StatusIndicatorState state() const { return state_; }
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-private:
-        void paintIcon(QPainter &p, QIcon &icon);
-
-        QIcon lockIcon_;
-        QIcon clockIcon_;
-        QIcon checkmarkIcon_;
-        QIcon doubleCheckmarkIcon_;
-
-        QColor iconColor_ = QColor("#999");
-
-        StatusIndicatorState state_ = StatusIndicatorState::Empty;
-
-        static constexpr int MaxWidth = 24;
-};
-
-class EventProxy : public QObject
-{
-        Q_OBJECT
-
-signals:
-        void eventRetrieved(const nlohmann::json &);
-};
-
-class UserProfileFilter : public QObject
-{
-        Q_OBJECT
-
-public:
-        explicit UserProfileFilter(const QString &user_id, QLabel *parent)
-          : QObject(parent)
-          , user_id_{user_id}
-        {}
-
-signals:
-        void hoverOff();
-        void hoverOn();
-        void clicked();
-
-protected:
-        bool eventFilter(QObject *obj, QEvent *event)
-        {
-                if (event->type() == QEvent::MouseButtonRelease) {
-                        emit clicked();
-                        return true;
-                } else if (event->type() == QEvent::HoverLeave) {
-                        emit hoverOff();
-                        return true;
-                } else if (event->type() == QEvent::HoverEnter) {
-                        emit hoverOn();
-                        return true;
-                }
-
-                return QObject::eventFilter(obj, event);
-        }
-
-private:
-        QString user_id_;
-};
-
-class TimelineItem : public QWidget
-{
-        Q_OBJECT
-        Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
-
-public:
-        TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-
-        // For local messages.
-        // m.text & m.emote
-        TimelineItem(mtx::events::MessageType ty,
-                     const QString &userid,
-                     QString body,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        // m.image
-        TimelineItem(ImageItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(FileItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(AudioItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineItem(VideoItem *item,
-                     const QString &userid,
-                     bool withSender,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-
-        TimelineItem(ImageItem *img,
-                     const mtx::events::RoomEvent<mtx::events::msg::Image> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(StickerItem *img,
-                     const mtx::events::Sticker &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(FileItem *file,
-                     const mtx::events::RoomEvent<mtx::events::msg::File> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(AudioItem *audio,
-                     const mtx::events::RoomEvent<mtx::events::msg::Audio> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-        TimelineItem(VideoItem *video,
-                     const mtx::events::RoomEvent<mtx::events::msg::Video> &e,
-                     bool with_sender,
-                     const QString &room_id,
-                     QWidget *parent);
-
-        ~TimelineItem();
-
-        void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        void setUserAvatar(const QString &userid);
-        DescInfo descriptionMessage() const { return descriptionMsg_; }
-        QString eventId() const { return event_id_; }
-        void setEventId(const QString &event_id) { event_id_ = event_id; }
-        void markReceived(bool isEncrypted);
-        void markRead();
-        void markSent();
-        bool isReceived() { return isReceived_; };
-        void setRoomId(QString room_id) { room_id_ = room_id; }
-        void sendReadReceipt() const;
-        void openRawMessageViewer() const;
-        void replyAction();
-
-        //! Add a user avatar for this event.
-        void addAvatar();
-        void addKeyRequestAction();
-
-signals:
-        void eventRedacted(const QString &event_id);
-        void redactionFailed(const QString &msg);
-
-public slots:
-        void refreshAuthorColor();
-        void finishedGeneratingColor();
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void contextMenuEvent(QContextMenuEvent *event) override;
-
-private:
-        //! If we are the sender of the message the event wil be marked as received by the server.
-        void markOwnMessagesAsReceived(const std::string &sender);
-        void init();
-        //! Add a context menu option to save the image of the timeline item.
-        void addSaveImageAction(ImageItem *image);
-        //! Add the reply action in the context menu for widgets that support it.
-        void addReplyAction();
-
-        template<class Widget>
-        void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender);
-
-        template<class Event, class Widget>
-        void setupWidgetLayout(Widget *widget, const Event &event, bool withSender);
-
-        void generateBody(const QString &body);
-        void generateBody(const QString &user_id, const QString &displayname, const QString &body);
-        void generateTimestamp(const QDateTime &time);
-        void generateUserName(const QString &userid, const QString &displayname);
-
-        void setupAvatarLayout(const QString &userName);
-        void setupSimpleLayout();
-
-        void adjustMessageLayout();
-        void adjustMessageLayoutForWidget();
-
-        //! Whether or not the event associated with the widget
-        //! has been acknowledged by the server.
-        bool isReceived_ = false;
-
-        QFutureWatcher<QString> *colorGenerating_;
-
-        QString event_id_;
-        mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown;
-        QString room_id_;
-
-        DescInfo descriptionMsg_;
-
-        QMenu *contextMenu_;
-        QAction *showReadReceipts_;
-        QAction *markAsRead_;
-        QAction *redactMsg_;
-        QAction *viewRawMessage_;
-        QAction *replyMsg_;
-
-        QHBoxLayout *topLayout_     = nullptr;
-        QHBoxLayout *messageLayout_ = nullptr;
-        QHBoxLayout *actionLayout_  = nullptr;
-        QVBoxLayout *mainLayout_    = nullptr;
-        QHBoxLayout *widgetLayout_  = nullptr;
-
-        Avatar *userAvatar_;
-
-        QFont timestampFont_;
-
-        StatusIndicator *statusIndicator_;
-
-        QLabel *timestamp_;
-        QLabel *userName_;
-        TextLabel *body_;
-
-        QColor backgroundColor_;
-
-        FlatButton *replyBtn_;
-        FlatButton *contextBtn_;
-};
-
-template<class Widget>
-void
-TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender)
-{
-        auto displayName = Cache::displayName(room_id_, userid);
-        auto timestamp   = QDateTime::currentDateTime();
-
-        descriptionMsg_ = {"", // No event_id up until this point.
-                           "You",
-                           userid,
-                           QString(" %1").arg(utils::messageDescription<Widget>()),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        widgetLayout_ = new QHBoxLayout;
-        widgetLayout_->setContentsMargins(0, 2, 0, 2);
-        widgetLayout_->addWidget(widget);
-        widgetLayout_->addStretch(1);
-
-        if (withSender) {
-                generateBody(userid, displayName, "");
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(userid);
-        } else {
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayoutForWidget();
-}
-
-template<class Event, class Widget>
-void
-TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender)
-{
-        init();
-
-        // if (event.type == mtx::events::EventType::RoomMessage) {
-        //        message_type_ = mtx::events::getMessageType(event.content.msgtype);
-        //}
-        // TODO: Fix this.
-        message_type_     = mtx::events::MessageType::Unknown;
-        event_id_         = QString::fromStdString(event.event_id);
-        const auto sender = QString::fromStdString(event.sender);
-
-        auto timestamp   = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
-        auto displayName = Cache::displayName(room_id_, sender);
-
-        QSettings settings;
-        descriptionMsg_ = {event_id_,
-                           sender == settings.value("auth/user_id") ? "You" : displayName,
-                           sender,
-                           QString(" %1").arg(utils::messageDescription<Widget>()),
-                           utils::descriptiveTime(timestamp),
-                           timestamp};
-
-        generateTimestamp(timestamp);
-
-        widgetLayout_ = new QHBoxLayout();
-        widgetLayout_->setContentsMargins(0, 2, 0, 2);
-        widgetLayout_->addWidget(widget);
-        widgetLayout_->addStretch(1);
-
-        if (withSender) {
-                generateBody(sender, displayName, "");
-                setupAvatarLayout(displayName);
-
-                setUserAvatar(sender);
-        } else {
-                setupSimpleLayout();
-        }
-
-        adjustMessageLayoutForWidget();
-}
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
new file mode 100644
index 00000000..ab7d3d47
--- /dev/null
+++ b/src/timeline/TimelineModel.cpp
@@ -0,0 +1,1220 @@
+#include "TimelineModel.h"
+
+#include <algorithm>
+#include <type_traits>
+
+#include <QRegularExpression>
+
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MainWindow.h"
+#include "Olm.h"
+#include "TimelineViewManager.h"
+#include "Utils.h"
+#include "dialogs/RawMessage.h"
+
+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
+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>
+{
+        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>
+QString
+eventUrl(const mtx::events::Event<T> &)
+{
+        return "";
+}
+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>
+{
+        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
+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;
+}
+
+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)
+{
+        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;
+        return eventHeight(e) / (double)w;
+}
+}
+
+TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
+  : QAbstractListModel(parent)
+  , room_id_(room_id)
+  , manager_(manager)
+{
+        connect(
+          this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents);
+        connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) {
+                pending.remove(txn_id);
+                failed.insert(txn_id);
+                int idx = idToIndex(txn_id);
+                if (idx < 0) {
+                        nhlog::ui()->warn("Failed index out of range");
+                        return;
+                }
+                emit dataChanged(index(idx, 0), index(idx, 0));
+        });
+        connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) {
+                int idx = idToIndex(txn_id);
+                if (idx < 0) {
+                        nhlog::ui()->warn("Sent index out of range");
+                        return;
+                }
+                eventOrder[idx] = event_id;
+                auto ev         = events.value(txn_id);
+                ev              = boost::apply_visitor(
+                  [event_id](const auto &e) -> mtx::events::collections::TimelineEvents {
+                          auto eventCopy     = e;
+                          eventCopy.event_id = event_id.toStdString();
+                          return eventCopy;
+                  },
+                  ev);
+                events.remove(txn_id);
+                events.insert(event_id, ev);
+
+                // mark our messages as read
+                readEvent(event_id.toStdString());
+
+                // ask to be notified for read receipts
+                cache::client()->addPendingReceipt(room_id_, event_id);
+
+                emit dataChanged(index(idx, 0), index(idx, 0));
+        });
+        connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) {
+                emit ChatPage::instance()->showNotification(msg);
+        });
+}
+
+QHash<int, QByteArray>
+TimelineModel::roleNames() const
+{
+        return {
+          {Section, "section"},
+          {Type, "type"},
+          {Body, "body"},
+          {FormattedBody, "formattedBody"},
+          {UserId, "userId"},
+          {UserName, "userName"},
+          {Timestamp, "timestamp"},
+          {Url, "url"},
+          {ThumbnailUrl, "thumbnailUrl"},
+          {Filename, "filename"},
+          {Filesize, "filesize"},
+          {MimeType, "mimetype"},
+          {Height, "height"},
+          {Width, "width"},
+          {ProportionalHeight, "proportionalHeight"},
+          {Id, "id"},
+          {State, "state"},
+          {IsEncrypted, "isEncrypted"},
+          {ReplyTo, "replyTo"},
+        };
+}
+int
+TimelineModel::rowCount(const QModelIndex &parent) const
+{
+        Q_UNUSED(parent);
+        return (int)this->eventOrder.size();
+}
+
+QVariant
+TimelineModel::data(const QModelIndex &index, int role) const
+{
+        if (index.row() < 0 && index.row() >= (int)eventOrder.size())
+                return QVariant();
+
+        QString id = eventOrder[index.row()];
+
+        mtx::events::collections::TimelineEvents event = events.value(id);
+
+        if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+                event = decryptEvent(*e).event;
+        }
+
+        switch (role) {
+        case Section: {
+                QDateTime date = boost::apply_visitor(
+                  [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event);
+                date.setTime(QTime());
+
+                QString userId =
+                  boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event);
+
+                for (int r = index.row() - 1; r > 0; r--) {
+                        QDateTime prevDate = boost::apply_visitor(
+                          [](const auto &e) -> QDateTime { return eventTimestamp(e); },
+                          events.value(eventOrder[r]));
+                        prevDate.setTime(QTime());
+                        if (prevDate != date)
+                                return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId);
+
+                        QString prevUserId =
+                          boost::apply_visitor([](const auto &e) -> QString { return senderId(e); },
+                                               events.value(eventOrder[r]));
+                        if (userId != prevUserId)
+                                break;
+                }
+
+                return QString("%1").arg(userId);
+        }
+        case UserId:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return senderId(e); }, event));
+        case UserName:
+                return QVariant(displayName(boost::apply_visitor(
+                  [](const auto &e) -> QString { return senderId(e); }, event)));
+
+        case Timestamp:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event));
+        case Type:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); },
+                  event));
+        case Body:
+                return QVariant(utils::replaceEmoji(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventBody(e); }, event)));
+        case FormattedBody:
+                return QVariant(
+                  utils::replaceEmoji(
+                    boost::apply_visitor(
+                      [](const auto &e) -> QString { return eventFormattedBody(e); }, event))
+                    .remove("<mx-reply>")
+                    .remove("</mx-reply>"));
+        case Url:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventUrl(e); }, event));
+        case ThumbnailUrl:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event));
+        case Filename:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventFilename(e); }, event));
+        case Filesize:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString {
+                          return utils::humanReadableFileSize(eventFilesize(e));
+                  },
+                  event));
+        case MimeType:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventMimeType(e); }, event));
+        case Height:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> qulonglong { return eventHeight(e); }, event));
+        case Width:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> qulonglong { return eventWidth(e); }, event));
+        case ProportionalHeight:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> double { return eventPropHeight(e); }, event));
+        case Id:
+                return id;
+        case State:
+                // only show read receipts for messages not from us
+                if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); },
+                                         event)
+                      .toStdString() != http::client()->user_id().to_string())
+                        return qml_mtx_events::Empty;
+                else if (failed.contains(id))
+                        return qml_mtx_events::Failed;
+                else if (pending.contains(id))
+                        return qml_mtx_events::Sent;
+                else if (read.contains(id) ||
+                         cache::client()->readReceipts(id, room_id_).size() > 1)
+                        return qml_mtx_events::Read;
+                else
+                        return qml_mtx_events::Received;
+        case IsEncrypted: {
+                auto tempEvent = events[id];
+                return boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+                         &tempEvent) != nullptr;
+        }
+        case ReplyTo: {
+                QString evId = boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventRelatesTo(e); }, event);
+                return QVariant(evId);
+        }
+        default:
+                return QVariant();
+        }
+}
+
+void
+TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
+{
+        if (isInitialSync) {
+                prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
+                isInitialSync     = false;
+        }
+
+        if (timeline.events.empty())
+                return;
+
+        std::vector<QString> ids = internalAddEvents(timeline.events);
+
+        if (ids.empty())
+                return;
+
+        beginInsertRows(QModelIndex(),
+                        static_cast<int>(this->eventOrder.size()),
+                        static_cast<int>(this->eventOrder.size() + ids.size() - 1));
+        this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
+        endInsertRows();
+
+        updateLastMessage();
+}
+
+void
+TimelineModel::updateLastMessage()
+{
+        auto event = events.value(eventOrder.back());
+        if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+                event = decryptEvent(*e).event;
+        }
+
+        auto description = utils::getMessageDescription(
+          event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
+        emit manager_->updateRoomsLastMessage(room_id_, description);
+}
+
+std::vector<QString>
+TimelineModel::internalAddEvents(
+  const std::vector<mtx::events::collections::TimelineEvents> &timeline)
+{
+        std::vector<QString> ids;
+        for (const auto &e : timeline) {
+                QString id =
+                  boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e);
+
+                if (this->events.contains(id)) {
+                        this->events.insert(id, e);
+                        int idx = idToIndex(id);
+                        emit dataChanged(index(idx, 0), index(idx, 0));
+                        continue;
+                }
+
+                if (auto redaction =
+                      boost::get<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
+                        QString redacts = QString::fromStdString(redaction->redacts);
+                        auto redacted   = std::find(eventOrder.begin(), eventOrder.end(), redacts);
+
+                        if (redacted != eventOrder.end()) {
+                                auto redactedEvent = boost::apply_visitor(
+                                  [](const auto &ev)
+                                    -> mtx::events::RoomEvent<mtx::events::msg::Redacted> {
+                                          mtx::events::RoomEvent<mtx::events::msg::Redacted>
+                                            replacement                = {};
+                                          replacement.event_id         = ev.event_id;
+                                          replacement.room_id          = ev.room_id;
+                                          replacement.sender           = ev.sender;
+                                          replacement.origin_server_ts = ev.origin_server_ts;
+                                          replacement.type             = ev.type;
+                                          return replacement;
+                                  },
+                                  e);
+                                events.insert(redacts, redactedEvent);
+
+                                int row = (int)std::distance(eventOrder.begin(), redacted);
+                                emit dataChanged(index(row, 0), index(row, 0));
+                        }
+
+                        continue; // don't insert redaction into timeline
+                }
+
+                this->events.insert(id, e);
+                ids.push_back(id);
+        }
+        return ids;
+}
+
+void
+TimelineModel::fetchHistory()
+{
+        if (paginationInProgress) {
+                nhlog::ui()->warn("Already loading older messages");
+                return;
+        }
+
+        paginationInProgress = true;
+        mtx::http::MessagesOpts opts;
+        opts.room_id = room_id_.toStdString();
+        opts.from    = prev_batch_token_.toStdString();
+
+        nhlog::ui()->info("Paginationg room {}", opts.room_id);
+
+        http::client()->messages(
+          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->error("failed to call /messages ({}): {} - {}",
+                                              opts.room_id,
+                                              mtx::errors::to_string(err->matrix_error.errcode),
+                                              err->matrix_error.error);
+                          paginationInProgress = false;
+                          return;
+                  }
+
+                  emit oldMessagesRetrieved(std::move(res));
+                  paginationInProgress = false;
+          });
+}
+
+void
+TimelineModel::setCurrentIndex(int index)
+{
+        auto oldIndex = idToIndex(currentId);
+        currentId     = indexToId(index);
+        emit currentIndexChanged(index);
+
+        if (oldIndex < index && !pending.contains(currentId)) {
+                readEvent(currentId.toStdString());
+        }
+}
+
+void
+TimelineModel::readEvent(const std::string &id)
+{
+        http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) {
+                if (err) {
+                        nhlog::net()->warn("failed to read_event ({}, {})",
+                                           room_id_.toStdString(),
+                                           currentId.toStdString());
+                }
+        });
+}
+
+void
+TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
+{
+        std::vector<QString> ids = internalAddEvents(msgs.chunk);
+
+        if (!ids.empty()) {
+                beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1));
+                this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
+                endInsertRows();
+        }
+
+        prev_batch_token_ = QString::fromStdString(msgs.end);
+}
+
+QColor
+TimelineModel::userColor(QString id, QColor background)
+{
+        if (!userColors.contains(id))
+                userColors.insert(
+                  id, QColor(utils::generateContrastingHexColor(id, background.name())));
+        return userColors.value(id);
+}
+
+QString
+TimelineModel::displayName(QString id) const
+{
+        return Cache::displayName(room_id_, id);
+}
+
+QString
+TimelineModel::avatarUrl(QString id) const
+{
+        return Cache::avatarUrl(room_id_, id);
+}
+
+QString
+TimelineModel::formatDateSeparator(QDate date) const
+{
+        auto now = QDateTime::currentDateTime();
+
+        QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
+
+        if (now.date().year() == date.year()) {
+                QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
+                fmt = fmt.remove(rx);
+        }
+
+        return date.toString(fmt);
+}
+
+QString
+TimelineModel::escapeEmoji(QString str) const
+{
+        return utils::replaceEmoji(str);
+}
+
+void
+TimelineModel::viewRawMessage(QString id) const
+{
+        std::string ev = utils::serialize_event(events.value(id)).dump(4);
+        auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
+        Q_UNUSED(dialog);
+}
+
+void
+
+TimelineModel::openUserProfile(QString userid) const
+{
+        MainWindow::instance()->openUserProfile(userid, room_id_);
+}
+
+DecryptionResult
+TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const
+{
+        MegolmSessionIndex index;
+        index.room_id    = room_id_.toStdString();
+        index.session_id = e.content.session_id;
+        index.sender_key = e.content.sender_key;
+
+        mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
+        dummy.origin_server_ts = e.origin_server_ts;
+        dummy.event_id         = e.event_id;
+        dummy.sender           = e.sender;
+        dummy.content.body =
+          tr("-- Encrypted Event (No keys found for decryption) --",
+             "Placeholder, when the message was not decrypted yet or can't be decrypted")
+            .toStdString();
+
+        try {
+                if (!cache::client()->inboundMegolmSessionExists(index)) {
+                        nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
+                                              index.room_id,
+                                              index.session_id,
+                                              e.sender);
+                        // TODO: request megolm session_id & session_key from the sender.
+                        return {dummy, false};
+                }
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
+                dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
+                                        "Placeholder, when the message can't be decrypted, because "
+                                        "the DB access failed when trying to lookup the session.")
+                                       .toStdString();
+                return {dummy, false};
+        }
+
+        std::string msg_str;
+        try {
+                auto session = cache::client()->getInboundMegolmSession(index);
+                auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
+                msg_str      = std::string((char *)res.data.data(), res.data.size());
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
+                                      index.room_id,
+                                      index.session_id,
+                                      index.sender_key,
+                                      e.what());
+                dummy.content.body =
+                  tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
+                     "Placeholder, when the message can't be decrypted, because the DB access "
+                     "failed.")
+                    .toStdString();
+                return {dummy, false};
+        } catch (const mtx::crypto::olm_exception &e) {
+                nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
+                                          index.room_id,
+                                          index.session_id,
+                                          index.sender_key,
+                                          e.what());
+                dummy.content.body =
+                  tr("-- Decryption Error (%1) --",
+                     "Placeholder, when the message can't be decrypted. In this case, the Olm "
+                     "decrytion returned an error, which is passed ad %1")
+                    .arg(e.what())
+                    .toStdString();
+                return {dummy, false};
+        }
+
+        // Add missing fields for the event.
+        json body                = json::parse(msg_str);
+        body["event_id"]         = e.event_id;
+        body["sender"]           = e.sender;
+        body["origin_server_ts"] = e.origin_server_ts;
+        body["unsigned"]         = e.unsigned_data;
+
+        json event_array = json::array();
+        event_array.push_back(body);
+
+        std::vector<mtx::events::collections::TimelineEvents> temp_events;
+        mtx::responses::utils::parse_timeline_events(event_array, temp_events);
+
+        if (temp_events.size() == 1)
+                return {temp_events.at(0), true};
+
+        dummy.content.body =
+          tr("-- Encrypted Event (Unknown event type) --",
+             "Placeholder, when the message was decrypted, but we couldn't parse it, because "
+             "Nheko/mtxclient don't support that event type yet")
+            .toStdString();
+        return {dummy, false};
+}
+
+void
+TimelineModel::replyAction(QString id)
+{
+        auto event          = events.value(id);
+        RelatedInfo related = boost::apply_visitor(
+          [](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(boost::apply_visitor(
+          [](const auto &e) -> std::string { return eventMsgType(e); }, event));
+        related.quoted_body = boost::apply_visitor(
+          [](const auto &e) -> QString { return eventFormattedBody(e); }, 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;
+
+        ChatPage::instance()->messageReply(related);
+}
+
+void
+TimelineModel::readReceiptsAction(QString id) const
+{
+        MainWindow::instance()->openReadReceiptsDialog(id);
+}
+
+void
+TimelineModel::redactEvent(QString id)
+{
+        if (!id.isEmpty())
+                http::client()->redact_event(
+                  room_id_.toStdString(),
+                  id.toStdString(),
+                  [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+                          if (err) {
+                                  emit redactionFailed(
+                                    tr("Message redaction failed: %1")
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+                                  return;
+                          }
+
+                          emit eventRedacted(id);
+                  });
+}
+
+int
+TimelineModel::idToIndex(QString id) const
+{
+        if (id.isEmpty())
+                return -1;
+        for (int i = 0; i < (int)eventOrder.size(); i++)
+                if (id == eventOrder[i])
+                        return i;
+        return -1;
+}
+
+QString
+TimelineModel::indexToId(int index) const
+{
+        if (index < 0 || index >= (int)eventOrder.size())
+                return "";
+        return eventOrder[index];
+}
+
+// Note: this will only be called for our messages
+void
+TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids)
+{
+        for (const auto &id : event_ids) {
+                read.insert(id);
+                int idx = idToIndex(id);
+                if (idx < 0) {
+                        nhlog::ui()->warn("Read index out of range");
+                        return;
+                }
+                emit dataChanged(index(idx, 0), index(idx, 0));
+        }
+}
+
+void
+TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content)
+{
+        const auto room_id = room_id_.toStdString();
+
+        using namespace mtx::events;
+        using namespace mtx::identifiers;
+
+        json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
+
+        try {
+                // Check if we have already an outbound megolm session then we can use.
+                if (cache::client()->outboundMegolmSessionExists(room_id)) {
+                        auto data = olm::encrypt_group_message(
+                          room_id, http::client()->device_id(), doc.dump());
+
+                        http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+                          room_id,
+                          txn_id,
+                          data,
+                          [this, txn_id](const mtx::responses::EventId &res,
+                                         mtx::http::RequestErr err) {
+                                  if (err) {
+                                          const int status_code =
+                                            static_cast<int>(err->status_code);
+                                          nhlog::net()->warn("[{}] failed to send message: {} {}",
+                                                             txn_id,
+                                                             err->matrix_error.error,
+                                                             status_code);
+                                          emit messageFailed(QString::fromStdString(txn_id));
+                                  }
+                                  emit messageSent(
+                                    QString::fromStdString(txn_id),
+                                    QString::fromStdString(res.event_id.to_string()));
+                          });
+                        return;
+                }
+
+                nhlog::ui()->debug("creating new outbound megolm session");
+
+                // Create a new outbound megolm session.
+                auto outbound_session  = olm::client()->init_outbound_group_session();
+                const auto session_id  = mtx::crypto::session_id(outbound_session.get());
+                const auto session_key = mtx::crypto::session_key(outbound_session.get());
+
+                // TODO: needs to be moved in the lib.
+                auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
+                                           {"room_id", room_id},
+                                           {"session_id", session_id},
+                                           {"session_key", session_key}};
+
+                // Saving the new megolm session.
+                // TODO: Maybe it's too early to save.
+                OutboundGroupSessionData session_data;
+                session_data.session_id    = session_id;
+                session_data.session_key   = session_key;
+                session_data.message_index = 0; // TODO Update me
+                cache::client()->saveOutboundMegolmSession(
+                  room_id, session_data, std::move(outbound_session));
+
+                const auto members = cache::client()->roomMembers(room_id);
+                nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
+
+                auto keeper =
+                  std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() {
+                          try {
+                                  auto data = olm::encrypt_group_message(
+                                    room_id, http::client()->device_id(), doc.dump());
+
+                                  http::client()
+                                    ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
+                                      room_id,
+                                      txn_id,
+                                      data,
+                                      [this, txn_id](const mtx::responses::EventId &res,
+                                                     mtx::http::RequestErr err) {
+                                              if (err) {
+                                                      const int status_code =
+                                                        static_cast<int>(err->status_code);
+                                                      nhlog::net()->warn(
+                                                        "[{}] failed to send message: {} {}",
+                                                        txn_id,
+                                                        err->matrix_error.error,
+                                                        status_code);
+                                                      emit messageFailed(
+                                                        QString::fromStdString(txn_id));
+                                              }
+                                              emit messageSent(
+                                                QString::fromStdString(txn_id),
+                                                QString::fromStdString(res.event_id.to_string()));
+                                      });
+                          } catch (const lmdb::error &e) {
+                                  nhlog::db()->critical(
+                                    "failed to save megolm outbound session: {}", e.what());
+                          }
+                  });
+
+                mtx::requests::QueryKeys req;
+                for (const auto &member : members)
+                        req.device_keys[member] = {};
+
+                http::client()->query_keys(
+                  req,
+                  [keeper = std::move(keeper), megolm_payload, this](
+                    const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::net()->warn("failed to query device keys: {} {}",
+                                                     err->matrix_error.error,
+                                                     static_cast<int>(err->status_code));
+                                  // TODO: Mark the event as failed. Communicate with the UI.
+                                  return;
+                          }
+
+                          for (const auto &user : res.device_keys) {
+                                  // Mapping from a device_id with valid identity keys to the
+                                  // generated room_key event used for sharing the megolm session.
+                                  std::map<std::string, std::string> room_key_msgs;
+                                  std::map<std::string, DevicePublicKeys> deviceKeys;
+
+                                  room_key_msgs.clear();
+                                  deviceKeys.clear();
+
+                                  for (const auto &dev : user.second) {
+                                          const auto user_id   = ::UserId(dev.second.user_id);
+                                          const auto device_id = DeviceId(dev.second.device_id);
+
+                                          const auto device_keys = dev.second.keys;
+                                          const auto curveKey    = "curve25519:" + device_id.get();
+                                          const auto edKey       = "ed25519:" + device_id.get();
+
+                                          if ((device_keys.find(curveKey) == device_keys.end()) ||
+                                              (device_keys.find(edKey) == device_keys.end())) {
+                                                  nhlog::net()->debug(
+                                                    "ignoring malformed keys for device {}",
+                                                    device_id.get());
+                                                  continue;
+                                          }
+
+                                          DevicePublicKeys pks;
+                                          pks.ed25519    = device_keys.at(edKey);
+                                          pks.curve25519 = device_keys.at(curveKey);
+
+                                          try {
+                                                  if (!mtx::crypto::verify_identity_signature(
+                                                        json(dev.second), device_id, user_id)) {
+                                                          nhlog::crypto()->warn(
+                                                            "failed to verify identity keys: {}",
+                                                            json(dev.second).dump(2));
+                                                          continue;
+                                                  }
+                                          } catch (const json::exception &e) {
+                                                  nhlog::crypto()->warn(
+                                                    "failed to parse device key json: {}",
+                                                    e.what());
+                                                  continue;
+                                          } catch (const mtx::crypto::olm_exception &e) {
+                                                  nhlog::crypto()->warn(
+                                                    "failed to verify device key json: {}",
+                                                    e.what());
+                                                  continue;
+                                          }
+
+                                          auto room_key = olm::client()
+                                                            ->create_room_key_event(
+                                                              user_id, pks.ed25519, megolm_payload)
+                                                            .dump();
+
+                                          room_key_msgs.emplace(device_id, room_key);
+                                          deviceKeys.emplace(device_id, pks);
+                                  }
+
+                                  std::vector<std::string> valid_devices;
+                                  valid_devices.reserve(room_key_msgs.size());
+                                  for (auto const &d : room_key_msgs) {
+                                          valid_devices.push_back(d.first);
+
+                                          nhlog::net()->info("{}", d.first);
+                                          nhlog::net()->info("  curve25519 {}",
+                                                             deviceKeys.at(d.first).curve25519);
+                                          nhlog::net()->info("  ed25519 {}",
+                                                             deviceKeys.at(d.first).ed25519);
+                                  }
+
+                                  nhlog::net()->info(
+                                    "sending claim request for user {} with {} devices",
+                                    user.first,
+                                    valid_devices.size());
+
+                                  http::client()->claim_keys(
+                                    user.first,
+                                    valid_devices,
+                                    std::bind(&TimelineModel::handleClaimedKeys,
+                                              this,
+                                              keeper,
+                                              room_key_msgs,
+                                              deviceKeys,
+                                              user.first,
+                                              std::placeholders::_1,
+                                              std::placeholders::_2));
+
+                                  // TODO: Wait before sending the next batch of requests.
+                                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
+                          }
+                  });
+
+                // TODO: Let the user know about the errors.
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical(
+                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
+        } catch (const mtx::crypto::olm_exception &e) {
+                nhlog::crypto()->critical(
+                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
+        }
+}
+
+void
+TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+                                 const std::map<std::string, std::string> &room_keys,
+                                 const std::map<std::string, DevicePublicKeys> &pks,
+                                 const std::string &user_id,
+                                 const mtx::responses::ClaimKeys &res,
+                                 mtx::http::RequestErr err)
+{
+        if (err) {
+                nhlog::net()->warn("claim keys error: {} {} {}",
+                                   err->matrix_error.error,
+                                   err->parse_error,
+                                   static_cast<int>(err->status_code));
+                return;
+        }
+
+        nhlog::net()->debug("claimed keys for {}", user_id);
+
+        if (res.one_time_keys.size() == 0) {
+                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
+                return;
+        }
+
+        if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
+                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
+                return;
+        }
+
+        auto retrieved_devices = res.one_time_keys.at(user_id);
+
+        // Payload with all the to_device message to be sent.
+        json body;
+        body["messages"][user_id] = json::object();
+
+        for (const auto &rd : retrieved_devices) {
+                const auto device_id = rd.first;
+                nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
+
+                // TODO: Verify signatures
+                auto otk = rd.second.begin()->at("key");
+
+                if (pks.find(device_id) == pks.end()) {
+                        nhlog::net()->critical("couldn't find public key for device: {}",
+                                               device_id);
+                        continue;
+                }
+
+                auto id_key = pks.at(device_id).curve25519;
+                auto s      = olm::client()->create_outbound_session(id_key, otk);
+
+                if (room_keys.find(device_id) == room_keys.end()) {
+                        nhlog::net()->critical("couldn't find m.room_key for device: {}",
+                                               device_id);
+                        continue;
+                }
+
+                auto device_msg = olm::client()->create_olm_encrypted_content(
+                  s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
+
+                try {
+                        cache::client()->saveOlmSession(id_key, std::move(s));
+                } catch (const lmdb::error &e) {
+                        nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
+                } catch (const mtx::crypto::olm_exception &e) {
+                        nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
+                                                  e.what());
+                }
+
+                body["messages"][user_id][device_id] = device_msg;
+        }
+
+        nhlog::net()->info("send_to_device: {}", user_id);
+
+        http::client()->send_to_device(
+          "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to send "
+                                             "send_to_device "
+                                             "message: {}",
+                                             err->matrix_error.error);
+                  }
+
+                  (void)keeper;
+          });
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
new file mode 100644
index 00000000..31e41315
--- /dev/null
+++ b/src/timeline/TimelineModel.h
@@ -0,0 +1,258 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QColor>
+#include <QDate>
+#include <QHash>
+#include <QSet>
+
+#include <mtx/responses.hpp>
+
+#include "Cache.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+
+namespace qml_mtx_events {
+Q_NAMESPACE
+
+enum EventType
+{
+        // Unsupported event
+        Unsupported,
+        /// m.room_key_request
+        KeyRequest,
+        /// m.room.aliases
+        Aliases,
+        /// m.room.avatar
+        Avatar,
+        /// m.room.canonical_alias
+        CanonicalAlias,
+        /// m.room.create
+        Create,
+        /// m.room.encrypted.
+        Encrypted,
+        /// m.room.encryption.
+        Encryption,
+        /// m.room.guest_access
+        GuestAccess,
+        /// m.room.history_visibility
+        HistoryVisibility,
+        /// m.room.join_rules
+        JoinRules,
+        /// m.room.member
+        Member,
+        /// m.room.name
+        Name,
+        /// m.room.power_levels
+        PowerLevels,
+        /// m.room.tombstone
+        Tombstone,
+        /// m.room.topic
+        Topic,
+        /// m.room.redaction
+        Redaction,
+        /// m.room.pinned_events
+        PinnedEvents,
+        // m.sticker
+        Sticker,
+        // m.tag
+        Tag,
+        /// m.room.message
+        AudioMessage,
+        EmoteMessage,
+        FileMessage,
+        ImageMessage,
+        LocationMessage,
+        NoticeMessage,
+        TextMessage,
+        VideoMessage,
+        Redacted,
+        UnknownMessage,
+};
+Q_ENUM_NS(EventType)
+
+enum EventState
+{
+        //! The plaintext message was received by the server.
+        Received,
+        //! At least one of the participants has read the message.
+        Read,
+        //! The client sent the message. Not yet received.
+        Sent,
+        //! When the message is loaded from cache or backfill.
+        Empty,
+        //! When the message failed to send
+        Failed,
+};
+Q_ENUM_NS(EventState)
+}
+
+class StateKeeper
+{
+public:
+        StateKeeper(std::function<void()> &&fn)
+          : fn_(std::move(fn))
+        {}
+
+        ~StateKeeper() { fn_(); }
+
+private:
+        std::function<void()> fn_;
+};
+
+struct DecryptionResult
+{
+        //! The decrypted content as a normal plaintext event.
+        mtx::events::collections::TimelineEvents event;
+        //! Whether or not the decryption was successful.
+        bool isDecrypted = false;
+};
+
+class TimelineViewManager;
+
+class TimelineModel : public QAbstractListModel
+{
+        Q_OBJECT
+        Q_PROPERTY(
+          int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
+
+public:
+        explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0);
+
+        enum Roles
+        {
+                Section,
+                Type,
+                Body,
+                FormattedBody,
+                UserId,
+                UserName,
+                Timestamp,
+                Url,
+                ThumbnailUrl,
+                Filename,
+                Filesize,
+                MimeType,
+                Height,
+                Width,
+                ProportionalHeight,
+                Id,
+                State,
+                IsEncrypted,
+                ReplyTo,
+        };
+
+        QHash<int, QByteArray> roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+        Q_INVOKABLE QColor userColor(QString id, QColor background);
+        Q_INVOKABLE QString displayName(QString id) const;
+        Q_INVOKABLE QString avatarUrl(QString id) const;
+        Q_INVOKABLE QString formatDateSeparator(QDate date) const;
+
+        Q_INVOKABLE QString escapeEmoji(QString str) const;
+        Q_INVOKABLE void viewRawMessage(QString id) const;
+        Q_INVOKABLE void openUserProfile(QString userid) const;
+        Q_INVOKABLE void replyAction(QString id);
+        Q_INVOKABLE void readReceiptsAction(QString id) const;
+        Q_INVOKABLE void redactEvent(QString id);
+        Q_INVOKABLE int idToIndex(QString id) const;
+        Q_INVOKABLE QString indexToId(int index) const;
+
+        void addEvents(const mtx::responses::Timeline &events);
+        template<class T>
+        void sendMessage(const T &msg);
+
+public slots:
+        void fetchHistory();
+        void setCurrentIndex(int index);
+        int currentIndex() const { return idToIndex(currentId); }
+        void markEventsAsRead(const std::vector<QString> &event_ids);
+
+private slots:
+        // Add old events at the top of the timeline.
+        void addBackwardsEvents(const mtx::responses::Messages &msgs);
+
+signals:
+        void oldMessagesRetrieved(const mtx::responses::Messages &res);
+        void messageFailed(QString txn_id);
+        void messageSent(QString txn_id, QString event_id);
+        void currentIndexChanged(int index);
+        void redactionFailed(QString id);
+        void eventRedacted(QString id);
+
+private:
+        DecryptionResult decryptEvent(
+          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const;
+        std::vector<QString> internalAddEvents(
+          const std::vector<mtx::events::collections::TimelineEvents> &timeline);
+        void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content);
+        void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+                               const std::map<std::string, std::string> &room_key,
+                               const std::map<std::string, DevicePublicKeys> &pks,
+                               const std::string &user_id,
+                               const mtx::responses::ClaimKeys &res,
+                               mtx::http::RequestErr err);
+        void updateLastMessage();
+        void readEvent(const std::string &id);
+
+        QHash<QString, mtx::events::collections::TimelineEvents> events;
+        QSet<QString> pending, failed, read;
+        std::vector<QString> eventOrder;
+
+        QString room_id_;
+        QString prev_batch_token_;
+
+        bool isInitialSync        = true;
+        bool paginationInProgress = false;
+
+        QHash<QString, QColor> userColors;
+        QString currentId;
+
+        TimelineViewManager *manager_;
+};
+
+template<class T>
+void
+TimelineModel::sendMessage(const T &msg)
+{
+        auto txn_id                       = http::client()->generate_txn_id();
+        mtx::events::RoomEvent<T> msgCopy = {};
+        msgCopy.content                   = msg;
+        msgCopy.type                      = mtx::events::EventType::RoomMessage;
+        msgCopy.event_id                  = txn_id;
+        msgCopy.sender                    = http::client()->user_id().to_string();
+        msgCopy.origin_server_ts          = QDateTime::currentMSecsSinceEpoch();
+        internalAddEvents({msgCopy});
+
+        QString txn_id_qstr = QString::fromStdString(txn_id);
+        beginInsertRows(QModelIndex(),
+                        static_cast<int>(this->eventOrder.size()),
+                        static_cast<int>(this->eventOrder.size()));
+        pending.insert(txn_id_qstr);
+        this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr);
+        endInsertRows();
+        updateLastMessage();
+
+        if (cache::client()->isRoomEncrypted(room_id_.toStdString()))
+                sendEncryptedMessage(txn_id, nlohmann::json(msg));
+        else
+                http::client()->send_room_message<T, mtx::events::EventType::RoomMessage>(
+                  room_id_.toStdString(),
+                  txn_id,
+                  msg,
+                  [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res,
+                                              mtx::http::RequestErr err) {
+                          if (err) {
+                                  const int status_code = static_cast<int>(err->status_code);
+                                  nhlog::net()->warn("[{}] failed to send message: {} {}",
+                                                     txn_id,
+                                                     err->matrix_error.error,
+                                                     status_code);
+                                  emit messageFailed(txn_id_qstr);
+                          }
+                          emit messageSent(txn_id_qstr,
+                                           QString::fromStdString(res.event_id.to_string()));
+                  });
+}
diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp
deleted file mode 100644
index ed783e90..00000000
--- a/src/timeline/TimelineView.cpp
+++ /dev/null
@@ -1,1627 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <boost/variant.hpp>
-
-#include <QApplication>
-#include <QFileInfo>
-#include <QTimer>
-#include <QtConcurrent>
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "Olm.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "ui/FloatingButton.h"
-#include "ui/InfoMessage.h"
-
-#include "timeline/TimelineView.h"
-#include "timeline/widgets/AudioItem.h"
-#include "timeline/widgets/FileItem.h"
-#include "timeline/widgets/ImageItem.h"
-#include "timeline/widgets/VideoItem.h"
-
-using TimelineEvent = mtx::events::collections::TimelineEvents;
-
-//! Maximum number of widgets to keep in the timeline layout.
-constexpr int MAX_RETAINED_WIDGETS = 100;
-constexpr int MIN_SCROLLBAR_HANDLE = 60;
-
-//! Retrieve the timestamp of the event represented by the given widget.
-QDateTime
-getDate(QWidget *widget)
-{
-        auto item = qobject_cast<TimelineItem *>(widget);
-        if (item)
-                return item->descriptionMessage().datetime;
-
-        auto infoMsg = qobject_cast<InfoMessage *>(widget);
-        if (infoMsg)
-                return infoMsg->datetime();
-
-        return QDateTime();
-}
-
-TimelineView::TimelineView(const mtx::responses::Timeline &timeline,
-                           const QString &room_id,
-                           QWidget *parent)
-  : QWidget(parent)
-  , room_id_{room_id}
-{
-        init();
-        addEvents(timeline);
-}
-
-TimelineView::TimelineView(const QString &room_id, QWidget *parent)
-  : QWidget(parent)
-  , room_id_{room_id}
-{
-        init();
-        getMessages();
-}
-
-void
-TimelineView::sliderRangeChanged(int min, int max)
-{
-        Q_UNUSED(min);
-
-        if (!scroll_area_->verticalScrollBar()->isVisible()) {
-                scroll_area_->verticalScrollBar()->setValue(max);
-                return;
-        }
-
-        // If the scrollbar is close to the bottom and a new message
-        // is added we move the scrollbar.
-        if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) {
-                scroll_area_->verticalScrollBar()->setValue(max);
-                return;
-        }
-
-        int currentHeight = scroll_widget_->size().height();
-        int diff          = currentHeight - oldHeight_;
-        int newPosition   = oldPosition_ + diff;
-
-        // Keep the scroll bar to the bottom if it hasn't been activated yet.
-        if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible())
-                newPosition = max;
-
-        if (lastMessageDirection_ == TimelineDirection::Top)
-                scroll_area_->verticalScrollBar()->setValue(newPosition);
-}
-
-void
-TimelineView::fetchHistory()
-{
-        if (!isScrollbarActivated() && !isTimelineFinished) {
-                if (!isVisible())
-                        return;
-
-                isPaginationInProgress_ = true;
-                getMessages();
-                paginationTimer_->start(2000);
-
-                return;
-        }
-
-        paginationTimer_->stop();
-}
-
-void
-TimelineView::scrollDown()
-{
-        int current = scroll_area_->verticalScrollBar()->value();
-        int max     = scroll_area_->verticalScrollBar()->maximum();
-
-        // The first time we enter the room move the scroll bar to the bottom.
-        if (!isInitialized) {
-                scroll_area_->verticalScrollBar()->setValue(max);
-                isInitialized = true;
-                return;
-        }
-
-        // If the gap is small enough move the scroll bar down. e.g when a new
-        // message appears.
-        if (max - current < SCROLL_BAR_GAP)
-                scroll_area_->verticalScrollBar()->setValue(max);
-}
-
-void
-TimelineView::sliderMoved(int position)
-{
-        if (!scroll_area_->verticalScrollBar()->isVisible())
-                return;
-
-        toggleScrollDownButton();
-
-        // The scrollbar is high enough so we can start retrieving old events.
-        if (position < SCROLL_BAR_GAP) {
-                if (isTimelineFinished)
-                        return;
-
-                // Prevent user from moving up when there is pagination in
-                // progress.
-                if (isPaginationInProgress_)
-                        return;
-
-                isPaginationInProgress_ = true;
-
-                getMessages();
-        }
-}
-
-bool
-TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs)
-{
-        return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start));
-}
-
-void
-TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs)
-{
-        // We've reached the start of the timline and there're no more messages.
-        if (isStartOfTimeline(msgs)) {
-                nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch",
-                                  room_id_.toStdString());
-                isTimelineFinished = true;
-                return;
-        }
-
-        isTimelineFinished = false;
-
-        // Queue incoming messages to be rendered later.
-        topMessages_.insert(topMessages_.end(),
-                            std::make_move_iterator(msgs.chunk.begin()),
-                            std::make_move_iterator(msgs.chunk.end()));
-
-        // The RoomList message preview will be updated only if this
-        // is the first batch of messages received through /messages
-        // i.e there are no other messages currently present.
-        if (!topMessages_.empty() && scroll_layout_->count() == 0)
-                notifyForLastEvent(findFirstViewableEvent(topMessages_));
-
-        if (isVisible()) {
-                renderTopEvents(topMessages_);
-
-                // Free up space for new messages.
-                topMessages_.clear();
-
-                // Send a read receipt for the last event.
-                if (isActiveWindow())
-                        readLastEvent();
-        }
-
-        prev_batch_token_       = QString::fromStdString(msgs.end);
-        isPaginationInProgress_ = false;
-}
-
-QWidget *
-TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
-                                TimelineDirection direction)
-{
-        using namespace mtx::events;
-
-        using AudioEvent  = RoomEvent<msg::Audio>;
-        using EmoteEvent  = RoomEvent<msg::Emote>;
-        using FileEvent   = RoomEvent<msg::File>;
-        using ImageEvent  = RoomEvent<msg::Image>;
-        using NoticeEvent = RoomEvent<msg::Notice>;
-        using TextEvent   = RoomEvent<msg::Text>;
-        using VideoEvent  = RoomEvent<msg::Video>;
-
-        if (boost::get<RedactionEvent<msg::Redaction>>(&event) != nullptr) {
-                auto redaction_event = boost::get<RedactionEvent<msg::Redaction>>(event);
-                const auto event_id  = QString::fromStdString(redaction_event.redacts);
-
-                QTimer::singleShot(0, this, [event_id, this]() {
-                        if (eventIds_.contains(event_id))
-                                removeEvent(event_id);
-                });
-
-                return nullptr;
-        } else if (boost::get<StateEvent<state::Encryption>>(&event) != nullptr) {
-                auto msg      = boost::get<StateEvent<state::Encryption>>(event);
-                auto event_id = QString::fromStdString(msg.event_id);
-
-                if (eventIds_.contains(event_id))
-                        return nullptr;
-
-                auto item = new InfoMessage(tr("Encryption is enabled"), this);
-                item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts));
-                eventIds_[event_id] = item;
-
-                // Force the next message to have avatar by not providing the current username.
-                saveMessageInfo("", msg.origin_server_ts, direction);
-
-                return item;
-        } else if (boost::get<RoomEvent<msg::Audio>>(&event) != nullptr) {
-                auto audio = boost::get<RoomEvent<msg::Audio>>(event);
-                return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
-        } else if (boost::get<RoomEvent<msg::Emote>>(&event) != nullptr) {
-                auto emote = boost::get<RoomEvent<msg::Emote>>(event);
-                return processMessageEvent<EmoteEvent>(emote, direction);
-        } else if (boost::get<RoomEvent<msg::File>>(&event) != nullptr) {
-                auto file = boost::get<RoomEvent<msg::File>>(event);
-                return processMessageEvent<FileEvent, FileItem>(file, direction);
-        } else if (boost::get<RoomEvent<msg::Image>>(&event) != nullptr) {
-                auto image = boost::get<RoomEvent<msg::Image>>(event);
-                return processMessageEvent<ImageEvent, ImageItem>(image, direction);
-        } else if (boost::get<RoomEvent<msg::Notice>>(&event) != nullptr) {
-                auto notice = boost::get<RoomEvent<msg::Notice>>(event);
-                return processMessageEvent<NoticeEvent>(notice, direction);
-        } else if (boost::get<RoomEvent<msg::Text>>(&event) != nullptr) {
-                auto text = boost::get<RoomEvent<msg::Text>>(event);
-                return processMessageEvent<TextEvent>(text, direction);
-        } else if (boost::get<RoomEvent<msg::Video>>(&event) != nullptr) {
-                auto video = boost::get<RoomEvent<msg::Video>>(event);
-                return processMessageEvent<VideoEvent, VideoItem>(video, direction);
-        } else if (boost::get<Sticker>(&event) != nullptr) {
-                return processMessageEvent<Sticker, StickerItem>(boost::get<Sticker>(event),
-                                                                 direction);
-        } else if (boost::get<EncryptedEvent<msg::Encrypted>>(&event) != nullptr) {
-                auto res = parseEncryptedEvent(boost::get<EncryptedEvent<msg::Encrypted>>(event));
-                auto widget = parseMessageEvent(res.event, direction);
-
-                if (widget == nullptr)
-                        return nullptr;
-
-                auto item = qobject_cast<TimelineItem *>(widget);
-
-                if (item && res.isDecrypted)
-                        item->markReceived(true);
-                else if (item && !res.isDecrypted)
-                        item->addKeyRequestAction();
-
-                return widget;
-        }
-
-        return nullptr;
-}
-
-DecryptionResult
-TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
-{
-        MegolmSessionIndex index;
-        index.room_id    = room_id_.toStdString();
-        index.session_id = e.content.session_id;
-        index.sender_key = e.content.sender_key;
-
-        mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
-        dummy.origin_server_ts = e.origin_server_ts;
-        dummy.event_id         = e.event_id;
-        dummy.sender           = e.sender;
-        dummy.content.body =
-          tr("-- Encrypted Event (No keys found for decryption) --",
-             "Placeholder, when the message was not decrypted yet or can't be decrypted")
-            .toStdString();
-
-        try {
-                if (!cache::client()->inboundMegolmSessionExists(index)) {
-                        nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
-                                              index.room_id,
-                                              index.session_id,
-                                              e.sender);
-                        // TODO: request megolm session_id & session_key from the sender.
-                        return {dummy, false};
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
-                dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
-                                        "Placeholder, when the message can't be decrypted, because "
-                                        "the DB access failed when trying to lookup the session.")
-                                       .toStdString();
-                return {dummy, false};
-        }
-
-        std::string msg_str;
-        try {
-                auto session = cache::client()->getInboundMegolmSession(index);
-                auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
-                msg_str      = std::string((char *)res.data.data(), res.data.size());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
-                                      index.room_id,
-                                      index.session_id,
-                                      index.sender_key,
-                                      e.what());
-                dummy.content.body =
-                  tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
-                     "Placeholder, when the message can't be decrypted, because the DB access "
-                     "failed.")
-                    .toStdString();
-                return {dummy, false};
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
-                                          index.room_id,
-                                          index.session_id,
-                                          index.sender_key,
-                                          e.what());
-                dummy.content.body =
-                  tr("-- Decryption Error (%1) --",
-                     "Placeholder, when the message can't be decrypted. In this case, the Olm "
-                     "decrytion returned an error, which is passed ad %1")
-                    .arg(e.what())
-                    .toStdString();
-                return {dummy, false};
-        }
-
-        // Add missing fields for the event.
-        json body                = json::parse(msg_str);
-        body["event_id"]         = e.event_id;
-        body["sender"]           = e.sender;
-        body["origin_server_ts"] = e.origin_server_ts;
-        body["unsigned"]         = e.unsigned_data;
-
-        nhlog::crypto()->debug("decrypted event: {}", e.event_id);
-
-        json event_array = json::array();
-        event_array.push_back(body);
-
-        std::vector<TimelineEvent> events;
-        mtx::responses::utils::parse_timeline_events(event_array, events);
-
-        if (events.size() == 1)
-                return {events.at(0), true};
-
-        dummy.content.body =
-          tr("-- Encrypted Event (Unknown event type) --",
-             "Placeholder, when the message was decrypted, but we couldn't parse it, because "
-             "Nheko/mtxclient don't support that event type yet")
-            .toStdString();
-        return {dummy, false};
-}
-
-void
-TimelineView::displayReadReceipts(std::vector<TimelineEvent> events)
-{
-        QtConcurrent::run(
-          [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() {
-                  std::vector<QString> event_ids;
-
-                  for (const auto &e : events) {
-                          if (utils::event_sender(e) == local_user)
-                                  event_ids.emplace_back(
-                                    QString::fromStdString(utils::event_id(e)));
-                  }
-
-                  auto readEvents =
-                    cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString());
-
-                  if (!readEvents.empty())
-                          emit markReadEvents(readEvents);
-          });
-}
-
-void
-TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
-{
-        int counter = 0;
-
-        for (const auto &event : events) {
-                QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom);
-
-                if (item != nullptr) {
-                        addTimelineItem(item, TimelineDirection::Bottom);
-                        counter++;
-
-                        // Prevent blocking of the event-loop
-                        // by calling processEvents every 10 items we render.
-                        if (counter % 4 == 0)
-                                QApplication::processEvents();
-                }
-        }
-
-        lastMessageDirection_ = TimelineDirection::Bottom;
-
-        displayReadReceipts(events);
-
-        QApplication::processEvents();
-}
-
-void
-TimelineView::renderTopEvents(const std::vector<TimelineEvent> &events)
-{
-        std::vector<QWidget *> items;
-
-        // Reset the sender of the first message in the timeline
-        // cause we're about to insert a new one.
-        firstSender_.clear();
-        firstMsgTimestamp_ = QDateTime();
-
-        // Parse in reverse order to determine where we should not show sender's name.
-        for (auto it = events.rbegin(); it != events.rend(); ++it) {
-                auto item = parseMessageEvent(*it, TimelineDirection::Top);
-
-                if (item != nullptr)
-                        items.push_back(item);
-        }
-
-        // Reverse again to render them.
-        std::reverse(items.begin(), items.end());
-
-        oldPosition_ = scroll_area_->verticalScrollBar()->value();
-        oldHeight_   = scroll_widget_->size().height();
-
-        for (const auto &item : items)
-                addTimelineItem(item, TimelineDirection::Top);
-
-        lastMessageDirection_ = TimelineDirection::Top;
-
-        QApplication::processEvents();
-
-        displayReadReceipts(events);
-
-        // If this batch is the first being rendered (i.e the first and the last
-        // events originate from this batch), set the last sender.
-        if (lastSender_.isEmpty() && !items.empty()) {
-                for (const auto &w : items) {
-                        auto timelineItem = qobject_cast<TimelineItem *>(w);
-                        if (timelineItem) {
-                                saveLastMessageInfo(timelineItem->descriptionMessage().userid,
-                                                    timelineItem->descriptionMessage().datetime);
-                                break;
-                        }
-                }
-        }
-}
-
-void
-TimelineView::addEvents(const mtx::responses::Timeline &timeline)
-{
-        if (isInitialSync) {
-                prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
-                isInitialSync     = false;
-        }
-
-        bottomMessages_.insert(bottomMessages_.end(),
-                               std::make_move_iterator(timeline.events.begin()),
-                               std::make_move_iterator(timeline.events.end()));
-
-        if (!bottomMessages_.empty())
-                notifyForLastEvent(findLastViewableEvent(bottomMessages_));
-
-        // If the current timeline is open and there are messages to be rendered.
-        if (isVisible() && !bottomMessages_.empty()) {
-                renderBottomEvents(bottomMessages_);
-
-                // Free up space for new messages.
-                bottomMessages_.clear();
-
-                // Send a read receipt for the last event.
-                if (isActiveWindow())
-                        readLastEvent();
-        }
-}
-
-void
-TimelineView::init()
-{
-        local_user_ = utils::localUser();
-
-        QIcon icon;
-        icon.addFile(":/icons/icons/ui/angle-arrow-down.png");
-        scrollDownBtn_ = new FloatingButton(icon, this);
-        scrollDownBtn_->hide();
-
-        connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() {
-                const int max = scroll_area_->verticalScrollBar()->maximum();
-                scroll_area_->verticalScrollBar()->setValue(max);
-        });
-        top_layout_ = new QVBoxLayout(this);
-        top_layout_->setSpacing(0);
-        top_layout_->setMargin(0);
-
-        scroll_area_ = new QScrollArea(this);
-        scroll_area_->setWidgetResizable(true);
-        scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-        scroll_widget_ = new QWidget(this);
-        scroll_widget_->setObjectName("scroll_widget");
-
-        // Height of the typing display.
-        QFont f;
-        f.setPointSizeF(f.pointSizeF() * 0.9);
-        const int bottomMargin = QFontMetrics(f).height() + 6;
-
-        scroll_layout_ = new QVBoxLayout(scroll_widget_);
-        scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
-        scroll_layout_->setSpacing(0);
-        scroll_layout_->setObjectName("timelinescrollarea");
-
-        scroll_area_->setWidget(scroll_widget_);
-        scroll_area_->setAlignment(Qt::AlignBottom);
-
-        top_layout_->addWidget(scroll_area_);
-
-        setLayout(top_layout_);
-
-        paginationTimer_ = new QTimer(this);
-        connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
-
-        connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents);
-
-        connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
-        connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage);
-
-        connect(
-          this, &TimelineView::markReadEvents, this, [this](const std::vector<QString> &event_ids) {
-                  for (const auto &event : event_ids) {
-                          if (eventIds_.contains(event)) {
-                                  auto widget = eventIds_[event];
-                                  if (!widget)
-                                          return;
-
-                                  auto item = qobject_cast<TimelineItem *>(widget);
-                                  if (!item)
-                                          return;
-
-                                  item->markRead();
-                          }
-                  }
-          });
-
-        connect(scroll_area_->verticalScrollBar(),
-                SIGNAL(valueChanged(int)),
-                this,
-                SLOT(sliderMoved(int)));
-        connect(scroll_area_->verticalScrollBar(),
-                SIGNAL(rangeChanged(int, int)),
-                this,
-                SLOT(sliderRangeChanged(int, int)));
-}
-
-void
-TimelineView::getMessages()
-{
-        mtx::http::MessagesOpts opts;
-        opts.room_id = room_id_.toStdString();
-        opts.from    = prev_batch_token_.toStdString();
-
-        http::client()->messages(
-          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->error("failed to call /messages ({}): {} - {}",
-                                              opts.room_id,
-                                              mtx::errors::to_string(err->matrix_error.errcode),
-                                              err->matrix_error.error);
-                          return;
-                  }
-
-                  emit messagesRetrieved(std::move(res));
-          });
-}
-
-void
-TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
-{
-        if (direction == TimelineDirection::Bottom)
-                lastSender_ = user_id;
-        else
-                firstSender_ = user_id;
-}
-
-bool
-TimelineView::isSenderRendered(const QString &user_id,
-                               uint64_t origin_server_ts,
-                               TimelineDirection direction)
-{
-        if (direction == TimelineDirection::Bottom) {
-                return (lastSender_ != user_id) ||
-                       isDateDifference(lastMsgTimestamp_,
-                                        QDateTime::fromMSecsSinceEpoch(origin_server_ts));
-        } else {
-                return (firstSender_ != user_id) ||
-                       isDateDifference(firstMsgTimestamp_,
-                                        QDateTime::fromMSecsSinceEpoch(origin_server_ts));
-        }
-}
-
-void
-TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction)
-{
-        const auto newDate = getDate(item);
-
-        if (direction == TimelineDirection::Bottom) {
-                QWidget *lastItem    = nullptr;
-                int lastItemPosition = 0;
-
-                if (scroll_layout_->count() > 0) {
-                        lastItemPosition = scroll_layout_->count() - 1;
-                        lastItem         = scroll_layout_->itemAt(lastItemPosition)->widget();
-                }
-
-                if (lastItem) {
-                        const auto oldDate = getDate(lastItem);
-
-                        if (oldDate.daysTo(newDate) != 0) {
-                                auto separator = new DateSeparator(newDate, this);
-
-                                if (separator)
-                                        pushTimelineItem(separator, direction);
-                        }
-                }
-
-                pushTimelineItem(item, direction);
-        } else {
-                if (scroll_layout_->count() > 0) {
-                        const auto firstItem = scroll_layout_->itemAt(0)->widget();
-
-                        if (firstItem) {
-                                const auto oldDate = getDate(firstItem);
-
-                                if (newDate.daysTo(oldDate) != 0) {
-                                        auto separator = new DateSeparator(oldDate);
-
-                                        if (separator)
-                                                pushTimelineItem(separator, direction);
-                                }
-                        }
-                }
-
-                pushTimelineItem(item, direction);
-        }
-}
-
-void
-TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id)
-{
-        nhlog::ui()->debug("[{}] message was received by the server", txn_id);
-        if (!pending_msgs_.isEmpty() &&
-            pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
-                auto msg     = pending_msgs_.dequeue();
-                msg.event_id = event_id;
-
-                if (msg.widget) {
-                        msg.widget->setEventId(event_id);
-                        eventIds_[event_id] = msg.widget;
-
-                        // If the response comes after we have received the event from sync
-                        // we've already marked the widget as received.
-                        if (!msg.widget->isReceived()) {
-                                msg.widget->markReceived(msg.is_encrypted);
-                                cache::client()->addPendingReceipt(room_id_, event_id);
-                                pending_sent_msgs_.append(msg);
-                        }
-                } else {
-                        nhlog::ui()->warn("[{}] received message response for invalid widget",
-                                          txn_id);
-                }
-        }
-
-        sendNextPendingMessage();
-}
-
-void
-TimelineView::addUserMessage(mtx::events::MessageType ty,
-                             const QString &body,
-                             const RelatedInfo &related = RelatedInfo())
-{
-        auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_);
-
-        QString full_body;
-        if (related.related_event.empty()) {
-                full_body = body;
-        } else {
-                full_body = utils::getFormattedQuoteBody(related, body);
-        }
-        TimelineItem *view_item =
-          new TimelineItem(ty, local_user_, full_body, with_sender, room_id_, scroll_widget_);
-
-        PendingMessage message;
-        message.ty      = ty;
-        message.txn_id  = http::client()->generate_txn_id();
-        message.body    = body;
-        message.related = related;
-        message.widget  = view_item;
-
-        try {
-                message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to check encryption status of room {}", e.what());
-                view_item->deleteLater();
-
-                // TODO: Send a notification to the user.
-
-                return;
-        }
-
-        addTimelineItem(view_item);
-
-        lastMessageDirection_ = TimelineDirection::Bottom;
-
-        saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
-        handleNewUserMessage(message);
-}
-
-void
-TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
-{
-        addUserMessage(ty, body, RelatedInfo());
-}
-
-void
-TimelineView::handleNewUserMessage(PendingMessage msg)
-{
-        pending_msgs_.enqueue(msg);
-        if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty())
-                sendNextPendingMessage();
-}
-
-void
-TimelineView::sendNextPendingMessage()
-{
-        if (pending_msgs_.size() == 0)
-                return;
-
-        using namespace mtx::events;
-
-        PendingMessage &m = pending_msgs_.head();
-
-        nhlog::ui()->debug("[{}] sending next queued message", m.txn_id);
-
-        if (m.widget)
-                m.widget->markSent();
-
-        if (m.is_encrypted) {
-                nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id);
-                prepareEncryptedMessage(std::move(m));
-                return;
-        }
-
-        switch (m.ty) {
-        case mtx::events::MessageType::Audio: {
-                http::client()->send_room_message<msg::Audio, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Audio>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Image: {
-                http::client()->send_room_message<msg::Image, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Image>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Video: {
-                http::client()->send_room_message<msg::Video, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Video>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::File: {
-                http::client()->send_room_message<msg::File, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::File>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Text: {
-                http::client()->send_room_message<msg::Text, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Text>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-
-                break;
-        }
-        case mtx::events::MessageType::Emote: {
-                http::client()->send_room_message<msg::Emote, EventType::RoomMessage>(
-                  room_id_.toStdString(),
-                  m.txn_id,
-                  toRoomMessage<msg::Emote>(m),
-                  std::bind(&TimelineView::sendRoomMessageHandler,
-                            this,
-                            m.txn_id,
-                            std::placeholders::_1,
-                            std::placeholders::_2));
-                break;
-        }
-        default:
-                nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString());
-                break;
-        }
-}
-
-void
-TimelineView::notifyForLastEvent()
-{
-        if (scroll_layout_->count() == 0) {
-                nhlog::ui()->error("notifyForLastEvent called with empty timeline");
-                return;
-        }
-
-        auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1);
-
-        if (!lastItem)
-                return;
-
-        auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget());
-
-        if (lastTimelineItem)
-                emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
-        else
-                nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString());
-}
-
-void
-TimelineView::notifyForLastEvent(const TimelineEvent &event)
-{
-        auto descInfo = utils::getMessageDescription(event, local_user_, room_id_);
-
-        if (!descInfo.timestamp.isEmpty())
-                emit updateLastTimelineMessage(room_id_, descInfo);
-}
-
-bool
-TimelineView::isPendingMessage(const std::string &txn_id,
-                               const QString &sender,
-                               const QString &local_userid)
-{
-        if (sender != local_userid)
-                return false;
-
-        auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; };
-
-        return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) ||
-               std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid);
-}
-
-void
-TimelineView::removePendingMessage(const std::string &txn_id)
-{
-        if (txn_id.empty())
-                return;
-
-        for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
-                if (it->txn_id == txn_id) {
-                        int index = std::distance(pending_sent_msgs_.begin(), it);
-                        pending_sent_msgs_.removeAt(index);
-
-                        if (pending_sent_msgs_.isEmpty())
-                                sendNextPendingMessage();
-
-                        nhlog::ui()->debug("[{}] removed message with sync", txn_id);
-                }
-        }
-        for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
-                if (it->txn_id == txn_id) {
-                        if (it->widget) {
-                                it->widget->markReceived(it->is_encrypted);
-
-                                // TODO: update when a solution for encrypted messages is available.
-                                if (!it->is_encrypted)
-                                        cache::client()->addPendingReceipt(room_id_, it->event_id);
-                        }
-
-                        nhlog::ui()->debug("[{}] received sync before message response", txn_id);
-                        return;
-                }
-        }
-}
-
-void
-TimelineView::handleFailedMessage(const std::string &txn_id)
-{
-        Q_UNUSED(txn_id);
-        // Note: We do this even if the message has already been echoed.
-        QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage()));
-}
-
-void
-TimelineView::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-TimelineView::readLastEvent() const
-{
-        if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled())
-                return;
-
-        const auto eventId = getLastEventId();
-
-        if (!eventId.isEmpty())
-                http::client()->read_event(room_id_.toStdString(),
-                                           eventId.toStdString(),
-                                           [this, eventId](mtx::http::RequestErr err) {
-                                                   if (err) {
-                                                           nhlog::net()->warn(
-                                                             "failed to read event ({}, {})",
-                                                             room_id_.toStdString(),
-                                                             eventId.toStdString());
-                                                   }
-                                           });
-}
-
-QString
-TimelineView::getLastEventId() const
-{
-        auto index = scroll_layout_->count();
-
-        // Search backwards for the first event that has a valid event id.
-        while (index > 0) {
-                --index;
-
-                auto lastItem          = scroll_layout_->itemAt(index);
-                auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget());
-
-                if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty())
-                        return lastTimelineItem->eventId();
-        }
-
-        return QString("");
-}
-
-void
-TimelineView::showEvent(QShowEvent *event)
-{
-        if (!topMessages_.empty()) {
-                renderTopEvents(topMessages_);
-                topMessages_.clear();
-        }
-
-        if (!bottomMessages_.empty()) {
-                renderBottomEvents(bottomMessages_);
-                bottomMessages_.clear();
-                scrollDown();
-        }
-
-        toggleScrollDownButton();
-
-        readLastEvent();
-
-        QWidget::showEvent(event);
-}
-
-void
-TimelineView::hideEvent(QHideEvent *event)
-{
-        const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height();
-        const auto widgetsNum   = scroll_layout_->count();
-
-        // Remove widgets from the timeline to reduce the memory footprint.
-        if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS)
-                clearTimeline();
-
-        QWidget::hideEvent(event);
-}
-
-bool
-TimelineView::event(QEvent *event)
-{
-        if (event->type() == QEvent::WindowActivate)
-                readLastEvent();
-
-        return QWidget::event(event);
-}
-
-void
-TimelineView::clearTimeline()
-{
-        // Delete all widgets.
-        QLayoutItem *item;
-        while ((item = scroll_layout_->takeAt(0)) != nullptr) {
-                delete item->widget();
-                delete item;
-        }
-
-        // The next call to /messages will be without a prev token.
-        prev_batch_token_.clear();
-        eventIds_.clear();
-
-        // Clear queues with pending messages to be rendered.
-        bottomMessages_.clear();
-        topMessages_.clear();
-
-        firstSender_.clear();
-        lastSender_.clear();
-}
-
-void
-TimelineView::toggleScrollDownButton()
-{
-        const int maxScroll     = scroll_area_->verticalScrollBar()->maximum();
-        const int currentScroll = scroll_area_->verticalScrollBar()->value();
-
-        if (maxScroll - currentScroll > SCROLL_BAR_GAP) {
-                scrollDownBtn_->show();
-                scrollDownBtn_->raise();
-        } else {
-                scrollDownBtn_->hide();
-        }
-}
-
-void
-TimelineView::removeEvent(const QString &event_id)
-{
-        if (!eventIds_.contains(event_id)) {
-                nhlog::ui()->warn("cannot remove widget with unknown event_id: {}",
-                                  event_id.toStdString());
-                return;
-        }
-
-        auto removedItem = eventIds_[event_id];
-
-        // Find the next and the previous widgets in the timeline
-        auto prevWidget = relativeWidget(removedItem, -1);
-        auto nextWidget = relativeWidget(removedItem, 1);
-
-        // See if they are timeline items
-        auto prevItem = qobject_cast<TimelineItem *>(prevWidget);
-        auto nextItem = qobject_cast<TimelineItem *>(nextWidget);
-
-        // ... or a date separator
-        auto prevLabel = qobject_cast<DateSeparator *>(prevWidget);
-
-        // If it's a TimelineItem add an avatar.
-        if (prevItem) {
-                prevItem->addAvatar();
-        }
-
-        if (nextItem) {
-                nextItem->addAvatar();
-        } else if (prevLabel) {
-                // If there's no chat message after this, and we have a label before us, delete the
-                // label.
-                prevLabel->deleteLater();
-        }
-
-        // If we deleted the last item in the timeline...
-        if (!nextItem && prevItem)
-                saveLastMessageInfo(prevItem->descriptionMessage().userid,
-                                    prevItem->descriptionMessage().datetime);
-
-        // If we deleted the first item in the timeline...
-        if (!prevItem && nextItem)
-                saveFirstMessageInfo(nextItem->descriptionMessage().userid,
-                                     nextItem->descriptionMessage().datetime);
-
-        // If we deleted the only item in the timeline...
-        if (!prevItem && !nextItem) {
-                firstSender_.clear();
-                firstMsgTimestamp_ = QDateTime();
-                lastSender_.clear();
-                lastMsgTimestamp_ = QDateTime();
-        }
-
-        // Finally remove the event.
-        removedItem->deleteLater();
-        eventIds_.remove(event_id);
-
-        // Update the room list with a view of the last message after
-        // all events have been processed.
-        QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); });
-}
-
-QWidget *
-TimelineView::relativeWidget(QWidget *item, int dt) const
-{
-        int pos = scroll_layout_->indexOf(item);
-
-        if (pos == -1)
-                return nullptr;
-
-        pos = pos + dt;
-
-        bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1);
-
-        return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget();
-}
-
-TimelineEvent
-TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events)
-{
-        auto it = std::find_if(events.begin(), events.end(), [](const auto &event) {
-                return mtx::events::EventType::RoomMessage == utils::event_type(event);
-        });
-
-        return (it == std::end(events)) ? events.front() : *it;
-}
-
-TimelineEvent
-TimelineView::findLastViewableEvent(const std::vector<TimelineEvent> &events)
-{
-        auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) {
-                return (mtx::events::EventType::RoomMessage == utils::event_type(event)) ||
-                       (mtx::events::EventType::RoomEncrypted == utils::event_type(event));
-        });
-
-        return (it == std::rend(events)) ? events.back() : *it;
-}
-
-void
-TimelineView::saveMessageInfo(const QString &sender,
-                              uint64_t origin_server_ts,
-                              TimelineDirection direction)
-{
-        updateLastSender(sender, direction);
-
-        if (direction == TimelineDirection::Bottom)
-                lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts);
-        else
-                firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts);
-}
-
-bool
-TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const
-{
-        // Check if the dates are in a different day.
-        if (std::abs(first.daysTo(second)) != 0)
-                return true;
-
-        const uint64_t diffInSeconds   = std::abs(first.msecsTo(second)) / 1000;
-        constexpr uint64_t fifteenMins = 15 * 60;
-
-        return diffInSeconds > fifteenMins;
-}
-
-void
-TimelineView::sendRoomMessageHandler(const std::string &txn_id,
-                                     const mtx::responses::EventId &res,
-                                     mtx::http::RequestErr err)
-{
-        if (err) {
-                const int status_code = static_cast<int>(err->status_code);
-                nhlog::net()->warn("[{}] failed to send message: {} {}",
-                                   txn_id,
-                                   err->matrix_error.error,
-                                   status_code);
-                emit messageFailed(txn_id);
-                return;
-        }
-
-        emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string()));
-}
-
-template<>
-mtx::events::msg::Audio
-toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m)
-{
-        mtx::events::msg::Audio audio;
-        audio.info.mimetype = m.mime.toStdString();
-        audio.info.size     = m.media_size;
-        audio.body          = m.filename.toStdString();
-        audio.url           = m.body.toStdString();
-        return audio;
-}
-
-template<>
-mtx::events::msg::Image
-toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m)
-{
-        mtx::events::msg::Image image;
-        image.info.mimetype = m.mime.toStdString();
-        image.info.size     = m.media_size;
-        image.body          = m.filename.toStdString();
-        image.url           = m.body.toStdString();
-        image.info.h        = m.dimensions.height();
-        image.info.w        = m.dimensions.width();
-        return image;
-}
-
-template<>
-mtx::events::msg::Video
-toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m)
-{
-        mtx::events::msg::Video video;
-        video.info.mimetype = m.mime.toStdString();
-        video.info.size     = m.media_size;
-        video.body          = m.filename.toStdString();
-        video.url           = m.body.toStdString();
-        return video;
-}
-
-template<>
-mtx::events::msg::Emote
-toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m)
-{
-        auto html = utils::markdownToHtml(m.body);
-
-        mtx::events::msg::Emote emote;
-        emote.body = m.body.trimmed().toStdString();
-
-        if (html != m.body.trimmed().toHtmlEscaped())
-                emote.formatted_body = html.toStdString();
-
-        return emote;
-}
-
-template<>
-mtx::events::msg::File
-toRoomMessage<mtx::events::msg::File>(const PendingMessage &m)
-{
-        mtx::events::msg::File file;
-        file.info.mimetype = m.mime.toStdString();
-        file.info.size     = m.media_size;
-        file.body          = m.filename.toStdString();
-        file.url           = m.body.toStdString();
-        return file;
-}
-
-template<>
-mtx::events::msg::Text
-toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m)
-{
-        auto html = utils::markdownToHtml(m.body);
-
-        mtx::events::msg::Text text;
-
-        text.body = m.body.trimmed().toStdString();
-
-        if (html != m.body.trimmed().toHtmlEscaped()) {
-                if (!m.related.quoted_body.isEmpty()) {
-                        text.formatted_body =
-                          utils::getFormattedQuoteBody(m.related, html).toStdString();
-                } else {
-                        text.formatted_body = html.toStdString();
-                }
-        }
-
-        if (!m.related.related_event.empty()) {
-                text.relates_to.in_reply_to.event_id = m.related.related_event;
-        }
-
-        return text;
-}
-
-void
-TimelineView::prepareEncryptedMessage(const PendingMessage &msg)
-{
-        const auto room_id = room_id_.toStdString();
-
-        using namespace mtx::events;
-        using namespace mtx::identifiers;
-
-        json content;
-
-        // Serialize the message to the plaintext that will be encrypted.
-        switch (msg.ty) {
-        case MessageType::Audio: {
-                content = json(toRoomMessage<msg::Audio>(msg));
-                break;
-        }
-        case MessageType::Emote: {
-                content = json(toRoomMessage<msg::Emote>(msg));
-                break;
-        }
-        case MessageType::File: {
-                content = json(toRoomMessage<msg::File>(msg));
-                break;
-        }
-        case MessageType::Image: {
-                content = json(toRoomMessage<msg::Image>(msg));
-                break;
-        }
-        case MessageType::Text: {
-                content = json(toRoomMessage<msg::Text>(msg));
-                break;
-        }
-        case MessageType::Video: {
-                content = json(toRoomMessage<msg::Video>(msg));
-                break;
-        }
-        default:
-                break;
-        }
-
-        json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
-
-        try {
-                // Check if we have already an outbound megolm session then we can use.
-                if (cache::client()->outboundMegolmSessionExists(room_id)) {
-                        auto data = olm::encrypt_group_message(
-                          room_id, http::client()->device_id(), doc.dump());
-
-                        http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
-                          room_id,
-                          msg.txn_id,
-                          data,
-                          std::bind(&TimelineView::sendRoomMessageHandler,
-                                    this,
-                                    msg.txn_id,
-                                    std::placeholders::_1,
-                                    std::placeholders::_2));
-                        return;
-                }
-
-                nhlog::ui()->debug("creating new outbound megolm session");
-
-                // Create a new outbound megolm session.
-                auto outbound_session  = olm::client()->init_outbound_group_session();
-                const auto session_id  = mtx::crypto::session_id(outbound_session.get());
-                const auto session_key = mtx::crypto::session_key(outbound_session.get());
-
-                // TODO: needs to be moved in the lib.
-                auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
-                                           {"room_id", room_id},
-                                           {"session_id", session_id},
-                                           {"session_key", session_key}};
-
-                // Saving the new megolm session.
-                // TODO: Maybe it's too early to save.
-                OutboundGroupSessionData session_data;
-                session_data.session_id    = session_id;
-                session_data.session_key   = session_key;
-                session_data.message_index = 0; // TODO Update me
-                cache::client()->saveOutboundMegolmSession(
-                  room_id, session_data, std::move(outbound_session));
-
-                const auto members = cache::client()->roomMembers(room_id);
-                nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
-
-                auto keeper = std::make_shared<StateKeeper>(
-                  [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() {
-                          try {
-                                  auto data = olm::encrypt_group_message(
-                                    room_id, http::client()->device_id(), doc.dump());
-
-                                  http::client()
-                                    ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
-                                      room_id,
-                                      txn_id,
-                                      data,
-                                      std::bind(&TimelineView::sendRoomMessageHandler,
-                                                this,
-                                                txn_id,
-                                                std::placeholders::_1,
-                                                std::placeholders::_2));
-
-                          } catch (const lmdb::error &e) {
-                                  nhlog::db()->critical(
-                                    "failed to save megolm outbound session: {}", e.what());
-                          }
-                  });
-
-                mtx::requests::QueryKeys req;
-                for (const auto &member : members)
-                        req.device_keys[member] = {};
-
-                http::client()->query_keys(
-                  req,
-                  [keeper = std::move(keeper), megolm_payload, this](
-                    const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::net()->warn("failed to query device keys: {} {}",
-                                                     err->matrix_error.error,
-                                                     static_cast<int>(err->status_code));
-                                  // TODO: Mark the event as failed. Communicate with the UI.
-                                  return;
-                          }
-
-                          for (const auto &user : res.device_keys) {
-                                  // Mapping from a device_id with valid identity keys to the
-                                  // generated room_key event used for sharing the megolm session.
-                                  std::map<std::string, std::string> room_key_msgs;
-                                  std::map<std::string, DevicePublicKeys> deviceKeys;
-
-                                  room_key_msgs.clear();
-                                  deviceKeys.clear();
-
-                                  for (const auto &dev : user.second) {
-                                          const auto user_id   = UserId(dev.second.user_id);
-                                          const auto device_id = DeviceId(dev.second.device_id);
-
-                                          const auto device_keys = dev.second.keys;
-                                          const auto curveKey    = "curve25519:" + device_id.get();
-                                          const auto edKey       = "ed25519:" + device_id.get();
-
-                                          if ((device_keys.find(curveKey) == device_keys.end()) ||
-                                              (device_keys.find(edKey) == device_keys.end())) {
-                                                  nhlog::net()->debug(
-                                                    "ignoring malformed keys for device {}",
-                                                    device_id.get());
-                                                  continue;
-                                          }
-
-                                          DevicePublicKeys pks;
-                                          pks.ed25519    = device_keys.at(edKey);
-                                          pks.curve25519 = device_keys.at(curveKey);
-
-                                          try {
-                                                  if (!mtx::crypto::verify_identity_signature(
-                                                        json(dev.second), device_id, user_id)) {
-                                                          nhlog::crypto()->warn(
-                                                            "failed to verify identity keys: {}",
-                                                            json(dev.second).dump(2));
-                                                          continue;
-                                                  }
-                                          } catch (const json::exception &e) {
-                                                  nhlog::crypto()->warn(
-                                                    "failed to parse device key json: {}",
-                                                    e.what());
-                                                  continue;
-                                          } catch (const mtx::crypto::olm_exception &e) {
-                                                  nhlog::crypto()->warn(
-                                                    "failed to verify device key json: {}",
-                                                    e.what());
-                                                  continue;
-                                          }
-
-                                          auto room_key = olm::client()
-                                                            ->create_room_key_event(
-                                                              user_id, pks.ed25519, megolm_payload)
-                                                            .dump();
-
-                                          room_key_msgs.emplace(device_id, room_key);
-                                          deviceKeys.emplace(device_id, pks);
-                                  }
-
-                                  std::vector<std::string> valid_devices;
-                                  valid_devices.reserve(room_key_msgs.size());
-                                  for (auto const &d : room_key_msgs) {
-                                          valid_devices.push_back(d.first);
-
-                                          nhlog::net()->info("{}", d.first);
-                                          nhlog::net()->info("  curve25519 {}",
-                                                             deviceKeys.at(d.first).curve25519);
-                                          nhlog::net()->info("  ed25519 {}",
-                                                             deviceKeys.at(d.first).ed25519);
-                                  }
-
-                                  nhlog::net()->info(
-                                    "sending claim request for user {} with {} devices",
-                                    user.first,
-                                    valid_devices.size());
-
-                                  http::client()->claim_keys(
-                                    user.first,
-                                    valid_devices,
-                                    std::bind(&TimelineView::handleClaimedKeys,
-                                              this,
-                                              keeper,
-                                              room_key_msgs,
-                                              deviceKeys,
-                                              user.first,
-                                              std::placeholders::_1,
-                                              std::placeholders::_2));
-
-                                  // TODO: Wait before sending the next batch of requests.
-                                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
-                          }
-                  });
-
-                // TODO: Let the user know about the errors.
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical(
-                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical(
-                  "failed to open outbound megolm session ({}): {}", room_id, e.what());
-        }
-}
-
-void
-TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
-                                const std::map<std::string, std::string> &room_keys,
-                                const std::map<std::string, DevicePublicKeys> &pks,
-                                const std::string &user_id,
-                                const mtx::responses::ClaimKeys &res,
-                                mtx::http::RequestErr err)
-{
-        if (err) {
-                nhlog::net()->warn("claim keys error: {} {} {}",
-                                   err->matrix_error.error,
-                                   err->parse_error,
-                                   static_cast<int>(err->status_code));
-                return;
-        }
-
-        nhlog::net()->debug("claimed keys for {}", user_id);
-
-        if (res.one_time_keys.size() == 0) {
-                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
-                return;
-        }
-
-        if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
-                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
-                return;
-        }
-
-        auto retrieved_devices = res.one_time_keys.at(user_id);
-
-        // Payload with all the to_device message to be sent.
-        json body;
-        body["messages"][user_id] = json::object();
-
-        for (const auto &rd : retrieved_devices) {
-                const auto device_id = rd.first;
-                nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
-
-                // TODO: Verify signatures
-                auto otk = rd.second.begin()->at("key");
-
-                if (pks.find(device_id) == pks.end()) {
-                        nhlog::net()->critical("couldn't find public key for device: {}",
-                                               device_id);
-                        continue;
-                }
-
-                auto id_key = pks.at(device_id).curve25519;
-                auto s      = olm::client()->create_outbound_session(id_key, otk);
-
-                if (room_keys.find(device_id) == room_keys.end()) {
-                        nhlog::net()->critical("couldn't find m.room_key for device: {}",
-                                               device_id);
-                        continue;
-                }
-
-                auto device_msg = olm::client()->create_olm_encrypted_content(
-                  s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
-
-                try {
-                        cache::client()->saveOlmSession(id_key, std::move(s));
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
-                } catch (const mtx::crypto::olm_exception &e) {
-                        nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
-                                                  e.what());
-                }
-
-                body["messages"][user_id][device_id] = device_msg;
-        }
-
-        nhlog::net()->info("send_to_device: {}", user_id);
-
-        http::client()->send_to_device(
-          "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to send "
-                                             "send_to_device "
-                                             "message: {}",
-                                             err->matrix_error.error);
-                  }
-
-                  (void)keeper;
-          });
-}
diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h
deleted file mode 100644
index 35796efd..00000000
--- a/src/timeline/TimelineView.h
+++ /dev/null
@@ -1,449 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QApplication>
-#include <QLayout>
-#include <QList>
-#include <QQueue>
-#include <QScrollArea>
-#include <QScrollBar>
-#include <QStyle>
-#include <QStyleOption>
-#include <QTimer>
-
-#include <mtx/events.hpp>
-#include <mtx/responses/messages.hpp>
-
-#include "../Utils.h"
-#include "MatrixClient.h"
-#include "timeline/TimelineItem.h"
-
-class StateKeeper
-{
-public:
-        StateKeeper(std::function<void()> &&fn)
-          : fn_(std::move(fn))
-        {}
-
-        ~StateKeeper() { fn_(); }
-
-private:
-        std::function<void()> fn_;
-};
-
-struct DecryptionResult
-{
-        //! The decrypted content as a normal plaintext event.
-        utils::TimelineEvent event;
-        //! Whether or not the decryption was successful.
-        bool isDecrypted = false;
-};
-
-class FloatingButton;
-struct DescInfo;
-
-// Contains info about a message shown in the history view
-// but not yet confirmed by the homeserver through sync.
-struct PendingMessage
-{
-        mtx::events::MessageType ty;
-        std::string txn_id;
-        RelatedInfo related;
-        QString body;
-        QString filename;
-        QString mime;
-        uint64_t media_size;
-        QString event_id;
-        TimelineItem *widget;
-        QSize dimensions;
-        bool is_encrypted = false;
-};
-
-template<class MessageT>
-MessageT
-toRoomMessage(const PendingMessage &) = delete;
-
-template<>
-mtx::events::msg::Audio
-toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::Emote
-toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::File
-toRoomMessage<mtx::events::msg::File>(const PendingMessage &);
-
-template<>
-mtx::events::msg::Image
-toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::Text
-toRoomMessage<mtx::events::msg::Text>(const PendingMessage &);
-
-template<>
-mtx::events::msg::Video
-toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m);
-
-// In which place new TimelineItems should be inserted.
-enum class TimelineDirection
-{
-        Top,
-        Bottom,
-};
-
-class TimelineView : public QWidget
-{
-        Q_OBJECT
-
-public:
-        TimelineView(const mtx::responses::Timeline &timeline,
-                     const QString &room_id,
-                     QWidget *parent = 0);
-        TimelineView(const QString &room_id, QWidget *parent = 0);
-
-        // Add new events at the end of the timeline.
-        void addEvents(const mtx::responses::Timeline &timeline);
-        void addUserMessage(mtx::events::MessageType ty,
-                            const QString &body,
-                            const RelatedInfo &related);
-        void addUserMessage(mtx::events::MessageType ty, const QString &msg);
-
-        template<class Widget, mtx::events::MessageType MsgType>
-        void addUserMessage(const QString &url,
-                            const QString &filename,
-                            const QString &mime,
-                            uint64_t size,
-                            const QSize &dimensions = QSize());
-        void updatePendingMessage(const std::string &txn_id, const QString &event_id);
-        void scrollDown();
-
-        //! Remove an item from the timeline with the given Event ID.
-        void removeEvent(const QString &event_id);
-        void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; }
-
-public slots:
-        void sliderRangeChanged(int min, int max);
-        void sliderMoved(int position);
-        void fetchHistory();
-
-        // Add old events at the top of the timeline.
-        void addBackwardsEvents(const mtx::responses::Messages &msgs);
-
-        // Whether or not the initial batch has been loaded.
-        bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; }
-
-        void handleFailedMessage(const std::string &txn_id);
-
-private slots:
-        void sendNextPendingMessage();
-
-signals:
-        void updateLastTimelineMessage(const QString &user, const DescInfo &info);
-        void messagesRetrieved(const mtx::responses::Messages &res);
-        void messageFailed(const std::string &txn_id);
-        void messageSent(const std::string &txn_id, const QString &event_id);
-        void markReadEvents(const std::vector<QString> &event_ids);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void showEvent(QShowEvent *event) override;
-        void hideEvent(QHideEvent *event) override;
-        bool event(QEvent *event) override;
-
-private:
-        using TimelineEvent = mtx::events::collections::TimelineEvents;
-
-        //! Mark our own widgets as read if they have more than one receipt.
-        void displayReadReceipts(std::vector<TimelineEvent> events);
-        //! Determine if the start of the timeline is reached from the response of /messages.
-        bool isStartOfTimeline(const mtx::responses::Messages &msgs);
-
-        QWidget *relativeWidget(QWidget *item, int dt) const;
-
-        DecryptionResult parseEncryptedEvent(
-          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
-
-        void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
-                               const std::map<std::string, std::string> &room_key,
-                               const std::map<std::string, DevicePublicKeys> &pks,
-                               const std::string &user_id,
-                               const mtx::responses::ClaimKeys &res,
-                               mtx::http::RequestErr err);
-
-        //! Callback for all message sending.
-        void sendRoomMessageHandler(const std::string &txn_id,
-                                    const mtx::responses::EventId &res,
-                                    mtx::http::RequestErr err);
-        void prepareEncryptedMessage(const PendingMessage &msg);
-
-        //! Call the /messages endpoint to fill the timeline.
-        void getMessages();
-        //! HACK: Fixing layout flickering when adding to the bottom
-        //! of the timeline.
-        void pushTimelineItem(QWidget *item, TimelineDirection dir)
-        {
-                setUpdatesEnabled(false);
-                item->hide();
-
-                if (dir == TimelineDirection::Top)
-                        scroll_layout_->insertWidget(0, item);
-                else
-                        scroll_layout_->addWidget(item);
-
-                QTimer::singleShot(0, this, [item, this]() {
-                        item->show();
-                        item->adjustSize();
-                        setUpdatesEnabled(true);
-                });
-        }
-
-        //! Decides whether or not to show or hide the scroll down button.
-        void toggleScrollDownButton();
-        void init();
-        void addTimelineItem(QWidget *item,
-                             TimelineDirection direction = TimelineDirection::Bottom);
-        void updateLastSender(const QString &user_id, TimelineDirection direction);
-        void notifyForLastEvent();
-        void notifyForLastEvent(const TimelineEvent &event);
-        //! Keep track of the sender and the timestamp of the current message.
-        void saveLastMessageInfo(const QString &sender, const QDateTime &datetime)
-        {
-                lastSender_       = sender;
-                lastMsgTimestamp_ = datetime;
-        }
-        void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime)
-        {
-                firstSender_       = sender;
-                firstMsgTimestamp_ = datetime;
-        }
-        //! Keep track of the sender and the timestamp of the current message.
-        void saveMessageInfo(const QString &sender,
-                             uint64_t origin_server_ts,
-                             TimelineDirection direction);
-
-        TimelineEvent findFirstViewableEvent(const std::vector<TimelineEvent> &events);
-        TimelineEvent findLastViewableEvent(const std::vector<TimelineEvent> &events);
-
-        //! Mark the last event as read.
-        void readLastEvent() const;
-        //! Whether or not the scrollbar is visible (non-zero height).
-        bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; }
-        //! Retrieve the event id of the last item.
-        QString getLastEventId() const;
-
-        template<class Event, class Widget>
-        TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
-
-        // TODO: Remove this eventually.
-        template<class Event>
-        TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
-
-        // For events with custom display widgets.
-        template<class Event, class Widget>
-        TimelineItem *createTimelineItem(const Event &event, bool withSender);
-
-        // For events without custom display widgets.
-        // TODO: All events should have custom widgets.
-        template<class Event>
-        TimelineItem *createTimelineItem(const Event &event, bool withSender);
-
-        // Used to determine whether or not we should prefix a message with the
-        // sender's name.
-        bool isSenderRendered(const QString &user_id,
-                              uint64_t origin_server_ts,
-                              TimelineDirection direction);
-
-        bool isPendingMessage(const std::string &txn_id,
-                              const QString &sender,
-                              const QString &userid);
-        void removePendingMessage(const std::string &txn_id);
-
-        bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); }
-
-        void handleNewUserMessage(PendingMessage msg);
-        bool isDateDifference(const QDateTime &first,
-                              const QDateTime &second = QDateTime::currentDateTime()) const;
-
-        // Return nullptr if the event couldn't be parsed.
-        QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
-                                   TimelineDirection direction);
-
-        //! Store the event id associated with the given widget.
-        void saveEventId(QWidget *widget);
-        //! Remove all widgets from the timeline layout.
-        void clearTimeline();
-
-        QVBoxLayout *top_layout_;
-        QVBoxLayout *scroll_layout_;
-
-        QScrollArea *scroll_area_;
-        QWidget *scroll_widget_;
-
-        QString firstSender_;
-        QDateTime firstMsgTimestamp_;
-        QString lastSender_;
-        QDateTime lastMsgTimestamp_;
-
-        QString room_id_;
-        QString prev_batch_token_;
-        QString local_user_;
-
-        bool isPaginationInProgress_ = false;
-
-        // Keeps track whether or not the user has visited the view.
-        bool isInitialized      = false;
-        bool isTimelineFinished = false;
-        bool isInitialSync      = true;
-
-        const int SCROLL_BAR_GAP = 200;
-
-        QTimer *paginationTimer_;
-
-        int scroll_height_       = 0;
-        int previous_max_height_ = 0;
-
-        int oldPosition_;
-        int oldHeight_;
-
-        FloatingButton *scrollDownBtn_;
-
-        TimelineDirection lastMessageDirection_;
-
-        //! Messages received by sync not added to the timeline.
-        std::vector<TimelineEvent> bottomMessages_;
-        //! Messages received by /messages not added to the timeline.
-        std::vector<TimelineEvent> topMessages_;
-
-        //! Render the given timeline events to the bottom of the timeline.
-        void renderBottomEvents(const std::vector<TimelineEvent> &events);
-        //! Render the given timeline events to the top of the timeline.
-        void renderTopEvents(const std::vector<TimelineEvent> &events);
-
-        // The events currently rendered. Used for duplicate detection.
-        QMap<QString, QWidget *> eventIds_;
-        QQueue<PendingMessage> pending_msgs_;
-        QList<PendingMessage> pending_sent_msgs_;
-};
-
-template<class Widget, mtx::events::MessageType MsgType>
-void
-TimelineView::addUserMessage(const QString &url,
-                             const QString &filename,
-                             const QString &mime,
-                             uint64_t size,
-                             const QSize &dimensions)
-{
-        auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_);
-        auto trimmed     = QFileInfo{filename}.fileName(); // Trim file path.
-
-        auto widget = new Widget(url, trimmed, size, this);
-
-        TimelineItem *view_item =
-          new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_);
-
-        addTimelineItem(view_item);
-
-        lastMessageDirection_ = TimelineDirection::Bottom;
-
-        // Keep track of the sender and the timestamp of the current message.
-        saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
-
-        PendingMessage message;
-        message.ty         = MsgType;
-        message.txn_id     = http::client()->generate_txn_id();
-        message.body       = url;
-        message.filename   = trimmed;
-        message.mime       = mime;
-        message.media_size = size;
-        message.widget     = view_item;
-        message.dimensions = dimensions;
-
-        handleNewUserMessage(message);
-}
-
-template<class Event>
-TimelineItem *
-TimelineView::createTimelineItem(const Event &event, bool withSender)
-{
-        TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_);
-        return item;
-}
-
-template<class Event, class Widget>
-TimelineItem *
-TimelineView::createTimelineItem(const Event &event, bool withSender)
-{
-        auto eventWidget = new Widget(event);
-        auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_);
-
-        return item;
-}
-
-template<class Event>
-TimelineItem *
-TimelineView::processMessageEvent(const Event &event, TimelineDirection direction)
-{
-        const auto event_id = QString::fromStdString(event.event_id);
-        const auto sender   = QString::fromStdString(event.sender);
-
-        const auto txn_id = event.unsigned_data.transaction_id;
-        if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
-            isDuplicate(event_id)) {
-                removePendingMessage(txn_id);
-                return nullptr;
-        }
-
-        auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction);
-
-        saveMessageInfo(sender, event.origin_server_ts, direction);
-
-        auto item = createTimelineItem<Event>(event, with_sender);
-
-        eventIds_[event_id] = item;
-
-        return item;
-}
-
-template<class Event, class Widget>
-TimelineItem *
-TimelineView::processMessageEvent(const Event &event, TimelineDirection direction)
-{
-        const auto event_id = QString::fromStdString(event.event_id);
-        const auto sender   = QString::fromStdString(event.sender);
-
-        const auto txn_id = event.unsigned_data.transaction_id;
-        if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
-            isDuplicate(event_id)) {
-                removePendingMessage(txn_id);
-                return nullptr;
-        }
-
-        auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction);
-
-        saveMessageInfo(sender, event.origin_server_ts, direction);
-
-        auto item = createTimelineItem<Event, Widget>(event, with_sender);
-
-        eventIds_[event_id] = item;
-
-        return item;
-}
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 86505481..d733ad90 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -1,340 +1,400 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <random>
-
-#include <QApplication>
-#include <QFileInfo>
-#include <QSettings>
-
-#include "Cache.h"
+#include "TimelineViewManager.h"
+
+#include <QFileDialog>
+#include <QMetaType>
+#include <QMimeDatabase>
+#include <QPalette>
+#include <QQmlContext>
+#include <QStandardPaths>
+
+#include "ChatPage.h"
+#include "ColorImageProvider.h"
+#include "DelegateChooser.h"
 #include "Logging.h"
-#include "Utils.h"
-#include "timeline/TimelineView.h"
-#include "timeline/TimelineViewManager.h"
-#include "timeline/widgets/AudioItem.h"
-#include "timeline/widgets/FileItem.h"
-#include "timeline/widgets/ImageItem.h"
-#include "timeline/widgets/VideoItem.h"
-
-TimelineViewManager::TimelineViewManager(QWidget *parent)
-  : QStackedWidget(parent)
-{}
+#include "MxcImageProvider.h"
+#include "UserSettingsPage.h"
+#include "dialogs/ImageOverlay.h"
 
 void
-TimelineViewManager::updateReadReceipts(const QString &room_id,
-                                        const std::vector<QString> &event_ids)
+TimelineViewManager::updateColorPalette()
 {
-        if (timelineViewExists(room_id)) {
-                auto view = views_[room_id];
-                if (view)
-                        emit view->markReadEvents(event_ids);
+        UserSettings settings;
+        if (settings.theme() == "light") {
+                QPalette lightActive(/*windowText*/ QColor("#333"),
+                                     /*button*/ QColor("#333"),
+                                     /*light*/ QColor(),
+                                     /*dark*/ QColor(220, 220, 220, 120),
+                                     /*mid*/ QColor(),
+                                     /*text*/ QColor("#333"),
+                                     /*bright_text*/ QColor(),
+                                     /*base*/ QColor("white"),
+                                     /*window*/ QColor("white"));
+                view->rootContext()->setContextProperty("currentActivePalette", lightActive);
+                view->rootContext()->setContextProperty("currentInactivePalette", lightActive);
+        } else if (settings.theme() == "dark") {
+                QPalette darkActive(/*windowText*/ QColor("#caccd1"),
+                                    /*button*/ QColor("#caccd1"),
+                                    /*light*/ QColor(),
+                                    /*dark*/ QColor(45, 49, 57, 120),
+                                    /*mid*/ QColor(),
+                                    /*text*/ QColor("#caccd1"),
+                                    /*bright_text*/ QColor(),
+                                    /*base*/ QColor("#202228"),
+                                    /*window*/ QColor("#202228"));
+                darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9"));
+                view->rootContext()->setContextProperty("currentActivePalette", darkActive);
+                view->rootContext()->setContextProperty("currentInactivePalette", darkActive);
+        } else {
+                view->rootContext()->setContextProperty("currentActivePalette", QPalette());
+                view->rootContext()->setContextProperty("currentInactivePalette", nullptr);
         }
 }
 
-void
-TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
+TimelineViewManager::TimelineViewManager(QWidget *parent)
+  : imgProvider(new MxcImageProvider())
+  , colorImgProvider(new ColorImageProvider())
 {
-        auto view = views_[room_id];
-
-        if (view)
-                view->removeEvent(event_id);
+        qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
+                                         "com.github.nheko",
+                                         1,
+                                         0,
+                                         "MtxEvent",
+                                         "Can't instantiate enum!");
+        qmlRegisterType<DelegateChoice>("com.github.nheko", 1, 0, "DelegateChoice");
+        qmlRegisterType<DelegateChooser>("com.github.nheko", 1, 0, "DelegateChooser");
+
+#ifdef USE_QUICK_VIEW
+        view      = new QQuickView();
+        container = QWidget::createWindowContainer(view, parent);
+#else
+        view      = new QQuickWidget(parent);
+        container = view;
+        view->setResizeMode(QQuickWidget::SizeRootObjectToView);
+        container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+        connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
+                nhlog::ui()->debug("Status changed to {}", status);
+        });
+#endif
+        container->setMinimumSize(200, 200);
+        view->rootContext()->setContextProperty("timelineManager", this);
+        updateColorPalette();
+        view->engine()->addImageProvider("MxcImage", imgProvider);
+        view->engine()->addImageProvider("colorimage", colorImgProvider);
+        view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
+
+        connect(dynamic_cast<ChatPage *>(parent),
+                &ChatPage::themeChanged,
+                this,
+                &TimelineViewManager::updateColorPalette);
 }
 
 void
-TimelineViewManager::queueTextMessage(const QString &msg)
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
 {
-        if (active_room_.isEmpty())
-                return;
-
-        auto room_id = active_room_;
-        auto view    = views_[room_id];
-
-        view->addUserMessage(mtx::events::MessageType::Text, msg);
+        for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
+                // addRoom will only add the room, if it doesn't exist
+                addRoom(QString::fromStdString(it->first));
+                models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline);
+        }
 }
 
 void
-TimelineViewManager::queueEmoteMessage(const QString &msg)
+TimelineViewManager::addRoom(const QString &room_id)
 {
-        if (active_room_.isEmpty())
-                return;
-
-        auto room_id = active_room_;
-        auto view    = views_[room_id];
-
-        view->addUserMessage(mtx::events::MessageType::Emote, msg);
+        if (!models.contains(room_id))
+                models.insert(room_id,
+                              QSharedPointer<TimelineModel>(new TimelineModel(this, room_id)));
 }
 
 void
-TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
+TimelineViewManager::setHistoryView(const QString &room_id)
 {
-        if (active_room_.isEmpty())
-                return;
+        nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
 
-        auto room_id = active_room_;
-        auto view    = views_[room_id];
-
-        view->addUserMessage(mtx::events::MessageType::Text, reply, related);
+        auto room = models.find(room_id);
+        if (room != models.end()) {
+                timeline_ = room.value().data();
+                emit activeTimelineChanged(timeline_);
+                nhlog::ui()->info("Activated room {}", room_id.toStdString());
+        }
 }
 
 void
-TimelineViewManager::queueImageMessage(const QString &roomid,
-                                       const QString &filename,
-                                       const QString &url,
-                                       const QString &mime,
-                                       uint64_t size,
-                                       const QSize &dimensions)
+TimelineViewManager::openImageOverlay(QString mxcUrl,
+                                      QString originalFilename,
+                                      QString mimeType,
+                                      qml_mtx_events::EventType eventType) const
 {
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("Cannot send m.image message to a non-managed view");
-                return;
-        }
-
-        auto view = views_[roomid];
-
-        view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(
-          url, filename, mime, size, dimensions);
+        QQuickImageResponse *imgResponse =
+          imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
+        connect(imgResponse,
+                &QQuickImageResponse::finished,
+                this,
+                [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() {
+                        if (!imgResponse->errorString().isEmpty()) {
+                                nhlog::ui()->error("Error when retrieving image for overlay: {}",
+                                                   imgResponse->errorString().toStdString());
+                                return;
+                        }
+                        auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
+
+                        auto imgDialog = new dialogs::ImageOverlay(pixmap);
+                        imgDialog->show();
+                        connect(imgDialog,
+                                &dialogs::ImageOverlay::saving,
+                                this,
+                                [this, mxcUrl, originalFilename, mimeType, eventType]() {
+                                        saveMedia(mxcUrl, originalFilename, mimeType, eventType);
+                                });
+                });
 }
 
 void
-TimelineViewManager::queueFileMessage(const QString &roomid,
-                                      const QString &filename,
-                                      const QString &url,
-                                      const QString &mime,
-                                      uint64_t size)
+TimelineViewManager::saveMedia(QString mxcUrl,
+                               QString originalFilename,
+                               QString mimeType,
+                               qml_mtx_events::EventType eventType) const
 {
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("cannot send m.file message to a non-managed view");
-                return;
+        QString dialogTitle;
+        if (eventType == qml_mtx_events::EventType::ImageMessage) {
+                dialogTitle = tr("Save image");
+        } else if (eventType == qml_mtx_events::EventType::VideoMessage) {
+                dialogTitle = tr("Save video");
+        } else if (eventType == qml_mtx_events::EventType::AudioMessage) {
+                dialogTitle = tr("Save audio");
+        } else {
+                dialogTitle = tr("Save file");
         }
 
-        auto view = views_[roomid];
+        QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
+
+        auto filename =
+          QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString);
+
+        if (filename.isEmpty())
+                return;
 
-        view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size);
+        const auto url = mxcUrl.toStdString();
+
+        http::client()->download(
+          url,
+          [filename, url](const std::string &data,
+                          const std::string &,
+                          const std::string &,
+                          mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
+                                             url,
+                                             err->matrix_error.error,
+                                             static_cast<int>(err->status_code));
+                          return;
+                  }
+
+                  try {
+                          QFile file(filename);
+
+                          if (!file.open(QIODevice::WriteOnly))
+                                  return;
+
+                          file.write(QByteArray(data.data(), data.size()));
+                          file.close();
+                  } catch (const std::exception &e) {
+                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
+                  }
+          });
 }
 
 void
-TimelineViewManager::queueAudioMessage(const QString &roomid,
-                                       const QString &filename,
-                                       const QString &url,
-                                       const QString &mime,
-                                       uint64_t size)
+TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType)
 {
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("cannot send m.audio message to a non-managed view");
+        // If the message is a link to a non mxcUrl, don't download it
+        if (!mxcUrl.startsWith("mxc://")) {
+                emit mediaCached(mxcUrl, mxcUrl);
                 return;
         }
 
-        auto view = views_[roomid];
-
-        view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size);
-}
+        QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
 
-void
-TimelineViewManager::queueVideoMessage(const QString &roomid,
-                                       const QString &filename,
-                                       const QString &url,
-                                       const QString &mime,
-                                       uint64_t size)
-{
-        if (!timelineViewExists(roomid)) {
-                nhlog::ui()->warn("cannot send m.video message to a non-managed view");
+        const auto url = mxcUrl.toStdString();
+        QFileInfo filename(QString("%1/media_cache/%2.%3")
+                             .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                             .arg(QString(mxcUrl).remove("mxc://"))
+                             .arg(suffix));
+        if (QDir::cleanPath(filename.path()) != filename.path()) {
+                nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
                 return;
         }
 
-        auto view = views_[roomid];
+        QDir().mkpath(filename.path());
 
-        view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size);
+        if (filename.isReadable()) {
+                emit mediaCached(mxcUrl, filename.filePath());
+                return;
+        }
+
+        http::client()->download(
+          url,
+          [this, mxcUrl, filename, url](const std::string &data,
+                                        const std::string &,
+                                        const std::string &,
+                                        mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
+                                             url,
+                                             err->matrix_error.error,
+                                             static_cast<int>(err->status_code));
+                          return;
+                  }
+
+                  try {
+                          QFile file(filename.filePath());
+
+                          if (!file.open(QIODevice::WriteOnly))
+                                  return;
+
+                          file.write(QByteArray(data.data(), data.size()));
+                          file.close();
+                  } catch (const std::exception &e) {
+                          nhlog::ui()->warn("Error while saving file to: {}", e.what());
+                  }
+
+                  emit mediaCached(mxcUrl, filename.filePath());
+          });
 }
 
 void
-TimelineViewManager::initialize(const mtx::responses::Rooms &rooms)
+TimelineViewManager::updateReadReceipts(const QString &room_id,
+                                        const std::vector<QString> &event_ids)
 {
-        for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
-                addRoom(it->second, QString::fromStdString(it->first));
+        auto room = models.find(room_id);
+        if (room != models.end()) {
+                room.value()->markEventsAsRead(event_ids);
         }
-
-        sync(rooms);
 }
 
 void
 TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
 {
-        for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) {
-                if (timelineViewExists(it->first))
-                        return;
-
-                // Create a history view with the room events.
-                TimelineView *view = new TimelineView(it->second, it->first);
-                views_.emplace(it->first, QSharedPointer<TimelineView>(view));
+        for (const auto &e : msgs) {
+                addRoom(e.first);
 
-                connect(view,
-                        &TimelineView::updateLastTimelineMessage,
-                        this,
-                        &TimelineViewManager::updateRoomsLastMessage);
-
-                // Add the view in the widget stack.
-                addWidget(view);
+                models.value(e.first)->addEvents(e.second);
         }
 }
 
 void
-TimelineViewManager::initialize(const std::vector<std::string> &rooms)
+TimelineViewManager::queueTextMessage(const QString &msg)
 {
-        for (const auto &roomid : rooms)
-                addRoom(QString::fromStdString(roomid));
+        mtx::events::msg::Text text = {};
+        text.body                   = msg.trimmed().toStdString();
+        text.format                 = "org.matrix.custom.html";
+        text.formatted_body         = utils::markdownToHtml(msg).toStdString();
+
+        if (timeline_)
+                timeline_->sendMessage(text);
 }
 
 void
-TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id)
+TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
 {
-        if (timelineViewExists(room_id))
-                return;
-
-        // Create a history view with the room events.
-        TimelineView *view = new TimelineView(room.timeline, room_id);
-        views_.emplace(room_id, QSharedPointer<TimelineView>(view));
+        mtx::events::msg::Text text = {};
+
+        QString body;
+        bool firstLine = true;
+        for (const auto &line : related.quoted_body.split("\n")) {
+                if (firstLine) {
+                        firstLine = false;
+                        body      = QString("> <%1> %2\n").arg(related.quoted_user).arg(line);
+                } else {
+                        body = QString("%1\n> %2\n").arg(body).arg(line);
+                }
+        }
 
-        connect(view,
-                &TimelineView::updateLastTimelineMessage,
-                this,
-                &TimelineViewManager::updateRoomsLastMessage);
+        text.body   = QString("%1\n%2").arg(body).arg(reply).toStdString();
+        text.format = "org.matrix.custom.html";
+        text.formatted_body =
+          utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString();
+        text.relates_to.in_reply_to.event_id = related.related_event;
 
-        // Add the view in the widget stack.
-        addWidget(view);
+        if (timeline_)
+                timeline_->sendMessage(text);
 }
 
 void
-TimelineViewManager::addRoom(const QString &room_id)
+TimelineViewManager::queueEmoteMessage(const QString &msg)
 {
-        if (timelineViewExists(room_id))
-                return;
+        auto html = utils::markdownToHtml(msg);
 
-        // Create a history view without any events.
-        TimelineView *view = new TimelineView(room_id);
-        views_.emplace(room_id, QSharedPointer<TimelineView>(view));
+        mtx::events::msg::Emote emote;
+        emote.body = msg.trimmed().toStdString();
 
-        connect(view,
-                &TimelineView::updateLastTimelineMessage,
-                this,
-                &TimelineViewManager::updateRoomsLastMessage);
+        if (html != msg.trimmed().toHtmlEscaped())
+                emote.formatted_body = html.toStdString();
 
-        // Add the view in the widget stack.
-        addWidget(view);
+        if (timeline_)
+                timeline_->sendMessage(emote);
 }
 
 void
-TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
+TimelineViewManager::queueImageMessage(const QString &roomid,
+                                       const QString &filename,
+                                       const QString &url,
+                                       const QString &mime,
+                                       uint64_t dsize,
+                                       const QSize &dimensions)
 {
-        for (const auto &room : rooms.join) {
-                auto roomid = QString::fromStdString(room.first);
-
-                if (!timelineViewExists(roomid)) {
-                        nhlog::ui()->warn("ignoring event from unknown room: {}",
-                                          roomid.toStdString());
-                        continue;
-                }
-
-                auto view = views_.at(roomid);
-
-                view->addEvents(room.second.timeline);
-        }
+        mtx::events::msg::Image image;
+        image.info.mimetype = mime.toStdString();
+        image.info.size     = dsize;
+        image.body          = filename.toStdString();
+        image.url           = url.toStdString();
+        image.info.h        = dimensions.height();
+        image.info.w        = dimensions.width();
+        models.value(roomid)->sendMessage(image);
 }
 
 void
-TimelineViewManager::setHistoryView(const QString &room_id)
+TimelineViewManager::queueFileMessage(const QString &roomid,
+                                      const QString &filename,
+                                      const QString &url,
+                                      const QString &mime,
+                                      uint64_t dsize)
 {
-        if (!timelineViewExists(room_id)) {
-                nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}",
-                                  room_id.toStdString());
-                return;
-        }
-
-        active_room_ = room_id;
-        auto view    = views_.at(room_id);
-
-        setCurrentWidget(view.data());
-
-        view->fetchHistory();
-        view->scrollDown();
+        mtx::events::msg::File file;
+        file.info.mimetype = mime.toStdString();
+        file.info.size     = dsize;
+        file.body          = filename.toStdString();
+        file.url           = url.toStdString();
+        models.value(roomid)->sendMessage(file);
 }
 
-QString
-TimelineViewManager::chooseRandomColor()
+void
+TimelineViewManager::queueAudioMessage(const QString &roomid,
+                                       const QString &filename,
+                                       const QString &url,
+                                       const QString &mime,
+                                       uint64_t dsize)
 {
-        std::random_device random_device;
-        std::mt19937 engine{random_device()};
-        std::uniform_real_distribution<float> dist(0, 1);
-
-        float hue        = dist(engine);
-        float saturation = 0.9;
-        float value      = 0.7;
-
-        int hue_i = hue * 6;
-
-        float f = hue * 6 - hue_i;
-
-        float p = value * (1 - saturation);
-        float q = value * (1 - f * saturation);
-        float t = value * (1 - (1 - f) * saturation);
-
-        float r = 0;
-        float g = 0;
-        float b = 0;
-
-        if (hue_i == 0) {
-                r = value;
-                g = t;
-                b = p;
-        } else if (hue_i == 1) {
-                r = q;
-                g = value;
-                b = p;
-        } else if (hue_i == 2) {
-                r = p;
-                g = value;
-                b = t;
-        } else if (hue_i == 3) {
-                r = p;
-                g = q;
-                b = value;
-        } else if (hue_i == 4) {
-                r = t;
-                g = p;
-                b = value;
-        } else if (hue_i == 5) {
-                r = value;
-                g = p;
-                b = q;
-        }
-
-        int ri = r * 256;
-        int gi = g * 256;
-        int bi = b * 256;
-
-        QColor color(ri, gi, bi);
-
-        return color.name();
+        mtx::events::msg::Audio audio;
+        audio.info.mimetype = mime.toStdString();
+        audio.info.size     = dsize;
+        audio.body          = filename.toStdString();
+        audio.url           = url.toStdString();
+        models.value(roomid)->sendMessage(audio);
 }
 
-bool
-TimelineViewManager::hasLoaded() const
+void
+TimelineViewManager::queueVideoMessage(const QString &roomid,
+                                       const QString &filename,
+                                       const QString &url,
+                                       const QString &mime,
+                                       uint64_t dsize)
 {
-        return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) {
-                return view.second->hasLoaded();
-        });
+        mtx::events::msg::Video video;
+        video.info.mimetype = mime.toStdString();
+        video.info.size     = dsize;
+        video.body          = filename.toStdString();
+        video.url           = url.toStdString();
+        models.value(roomid)->sendMessage(video);
 }
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b52136d9..691c8ddb 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -1,69 +1,80 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 #pragma once
 
+#include <QQuickView>
+#include <QQuickWidget>
 #include <QSharedPointer>
-#include <QStackedWidget>
+#include <QWidget>
 
-#include <mtx.hpp>
+#include <mtx/responses.hpp>
 
+#include "Cache.h"
+#include "Logging.h"
+#include "TimelineModel.h"
 #include "Utils.h"
 
-class QFile;
+// temporary for stubs
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
 
-class RoomInfoListItem;
-class TimelineView;
-struct DescInfo;
-struct SavedMessages;
+class MxcImageProvider;
+class ColorImageProvider;
 
-class TimelineViewManager : public QStackedWidget
+class TimelineViewManager : public QObject
 {
         Q_OBJECT
 
-public:
-        TimelineViewManager(QWidget *parent);
-
-        // Initialize with timeline events.
-        void initialize(const mtx::responses::Rooms &rooms);
-        // Empty initialization.
-        void initialize(const std::vector<std::string> &rooms);
+        Q_PROPERTY(
+          TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
 
-        void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id);
-        void addRoom(const QString &room_id);
+public:
+        TimelineViewManager(QWidget *parent = 0);
+        QWidget *getWidget() const { return container; }
 
         void sync(const mtx::responses::Rooms &rooms);
-        void clearAll() { views_.clear(); }
-
-        // Check if all the timelines have been loaded.
-        bool hasLoaded() const;
+        void addRoom(const QString &room_id);
 
-        static QString chooseRandomColor();
+        void clearAll() { models.clear(); }
+
+        Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
+        void openImageOverlay(QString mxcUrl,
+                              QString originalFilename,
+                              QString mimeType,
+                              qml_mtx_events::EventType eventType) const;
+        void saveMedia(QString mxcUrl,
+                       QString originalFilename,
+                       QString mimeType,
+                       qml_mtx_events::EventType eventType) const;
+        Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType);
+        // Qml can only pass enum as int
+        Q_INVOKABLE void openImageOverlay(QString mxcUrl,
+                                          QString originalFilename,
+                                          QString mimeType,
+                                          int eventType) const
+        {
+                openImageOverlay(
+                  mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
+        }
+        Q_INVOKABLE void saveMedia(QString mxcUrl,
+                                   QString originalFilename,
+                                   QString mimeType,
+                                   int eventType) const
+        {
+                saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
+        }
 
 signals:
         void clearRoomMessageCount(QString roomid);
-        void updateRoomsLastMessage(const QString &user, const DescInfo &info);
+        void updateRoomsLastMessage(QString roomid, const DescInfo &info);
+        void activeTimelineChanged(TimelineModel *timeline);
+        void mediaCached(QString mxcUrl, QString cacheUrl);
 
 public slots:
         void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
-        void removeTimelineEvent(const QString &room_id, const QString &event_id);
         void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);
 
         void setHistoryView(const QString &room_id);
+        void updateColorPalette();
+
         void queueTextMessage(const QString &msg);
         void queueReplyMessage(const QString &reply, const RelatedInfo &related);
         void queueEmoteMessage(const QString &msg);
@@ -90,9 +101,17 @@ public slots:
                                uint64_t dsize);
 
 private:
-        //! Check if the given room id is managed by a TimelineView.
-        bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }
-
-        QString active_room_;
-        std::map<QString, QSharedPointer<TimelineView>> views_;
+#ifdef USE_QUICK_VIEW
+        QQuickView *view;
+#else
+        QQuickWidget *view;
+#endif
+        QWidget *container;
+        TimelineModel *timeline_ = nullptr;
+        MxcImageProvider *imgProvider;
+        ColorImageProvider *colorImgProvider;
+
+        QHash<QString, QSharedPointer<TimelineModel>> models;
 };
+
+#pragma GCC diagnostic pop
diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp
deleted file mode 100644
index 5d6431ee..00000000
--- a/src/timeline/widgets/AudioItem.cpp
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFile>
-#include <QFileDialog>
-#include <QPainter>
-#include <QPixmap>
-#include <QtGlobal>
-
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-
-#include "timeline/widgets/AudioItem.h"
-
-constexpr int MaxWidth          = 400;
-constexpr int Height            = 70;
-constexpr int IconRadius        = 22;
-constexpr int IconDiameter      = IconRadius * 2;
-constexpr int HorizontalPadding = 12;
-constexpr int TextPadding       = 15;
-constexpr int ActionIconRadius  = IconRadius - 4;
-
-constexpr double VerticalPadding = Height - 2 * IconRadius;
-constexpr double IconYCenter     = Height / 2;
-constexpr double IconXCenter     = HorizontalPadding + IconRadius;
-
-void
-AudioItem::init()
-{
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
-
-        playIcon_.addFile(":/icons/icons/ui/play-sign.png");
-        pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
-
-        player_ = new QMediaPlayer;
-        player_->setMedia(QUrl(url_));
-        player_->setVolume(100);
-        player_->setNotifyInterval(1000);
-
-        connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
-                if (state == QMediaPlayer::StoppedState) {
-                        state_ = AudioState::Play;
-                        player_->setMedia(QUrl(url_));
-                        update();
-                }
-        });
-
-        setFixedHeight(Height);
-}
-
-AudioItem::AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, QWidget *parent)
-  : QWidget(parent)
-  , url_{QUrl(QString::fromStdString(event.content.url))}
-  , text_{QString::fromStdString(event.content.body)}
-  , event_{event}
-{
-        readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
-
-        init();
-}
-
-AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        readableFileSize_ = utils::humanReadableFileSize(size);
-
-        init();
-}
-
-QSize
-AudioItem::sizeHint() const
-{
-        return QSize(MaxWidth, Height);
-}
-
-void
-AudioItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->button() != Qt::LeftButton)
-                return;
-
-        auto point = event->pos();
-
-        // Click on the download icon.
-        if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter)
-              .contains(point)) {
-                if (state_ == AudioState::Play) {
-                        state_ = AudioState::Pause;
-                        player_->play();
-                } else {
-                        state_ = AudioState::Play;
-                        player_->pause();
-                }
-
-                update();
-        } else {
-                filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_);
-
-                if (filenameToSave_.isEmpty())
-                        return;
-
-                auto proxy = std::make_shared<MediaProxy>();
-                connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded);
-
-                http::client()->download(
-                  url_.toString().toStdString(),
-                  [proxy = std::move(proxy), url = url_](const std::string &data,
-                                                         const std::string &,
-                                                         const std::string &,
-                                                         mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::net()->info("failed to retrieve m.audio content: {}",
-                                                     url.toString().toStdString());
-                                  return;
-                          }
-
-                          emit proxy->fileDownloaded(QByteArray(data.data(), data.size()));
-                  });
-        }
-}
-
-void
-AudioItem::fileDownloaded(const QByteArray &data)
-{
-        try {
-                QFile file(filenameToSave_);
-
-                if (!file.open(QIODevice::WriteOnly))
-                        return;
-
-                file.write(data);
-                file.close();
-        } catch (const std::exception &e) {
-                nhlog::ui()->warn("error while saving file: {}", e.what());
-        }
-}
-
-void
-AudioItem::resizeEvent(QResizeEvent *event)
-{
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-        const int computedWidth = std::min(
-          fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth);
-#else
-        const int computedWidth =
-          std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding,
-                   (double)MaxWidth);
-#endif
-        resize(computedWidth, Height);
-
-        event->accept();
-}
-
-void
-AudioItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-
-        QPainterPath path;
-        path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(path, backgroundColor_);
-        painter.drawPath(path);
-
-        QPainterPath circle;
-        circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(circle, iconColor_);
-        painter.drawPath(circle);
-
-        QIcon icon_;
-        if (state_ == AudioState::Play)
-                icon_ = playIcon_;
-        else
-                icon_ = pauseIcon_;
-
-        icon_.paint(&painter,
-                    QRect(IconXCenter - ActionIconRadius / 2,
-                          IconYCenter - ActionIconRadius / 2,
-                          ActionIconRadius,
-                          ActionIconRadius),
-                    Qt::AlignCenter,
-                    QIcon::Normal);
-
-        const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding;
-        const int textStartY = VerticalPadding + fm.ascent() / 2;
-
-        // Draw the filename.
-        QString elidedText = fm.elidedText(
-          text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius);
-
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY), elidedText);
-
-        // Draw the filesize.
-        font.setWeight(QFont::Normal);
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_);
-}
diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h
deleted file mode 100644
index c32b7731..00000000
--- a/src/timeline/widgets/AudioItem.h
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QIcon>
-#include <QMediaPlayer>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-class AudioItem : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-
-        Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ
-                     durationBackgroundColor)
-        Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ
-                     durationForegroundColor)
-
-public:
-        AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
-                  QWidget *parent = nullptr);
-
-        AudioItem(const QString &url,
-                  const QString &filename,
-                  uint64_t size,
-                  QWidget *parent = nullptr);
-
-        QSize sizeHint() const override;
-
-        void setTextColor(const QColor &color) { textColor_ = color; }
-        void setIconColor(const QColor &color) { iconColor_ = color; }
-        void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
-
-        void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; }
-        void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; }
-
-        QColor textColor() const { return textColor_; }
-        QColor iconColor() const { return iconColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        QColor durationBackgroundColor() const { return durationBgColor_; }
-        QColor durationForegroundColor() const { return durationFgColor_; }
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
-
-private slots:
-        void fileDownloaded(const QByteArray &data);
-
-private:
-        void init();
-
-        enum class AudioState
-        {
-                Play,
-                Pause,
-        };
-
-        AudioState state_ = AudioState::Play;
-
-        QUrl url_;
-        QString text_;
-        QString readableFileSize_;
-        QString filenameToSave_;
-
-        mtx::events::RoomEvent<mtx::events::msg::Audio> event_;
-
-        QMediaPlayer *player_;
-
-        QIcon playIcon_;
-        QIcon pauseIcon_;
-
-        QColor textColor_       = QColor("white");
-        QColor iconColor_       = QColor("#38A3D8");
-        QColor backgroundColor_ = QColor("#333");
-
-        QColor durationBgColor_ = QColor("black");
-        QColor durationFgColor_ = QColor("blue");
-};
diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp
deleted file mode 100644
index 1a555d1c..00000000
--- a/src/timeline/widgets/FileItem.cpp
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFile>
-#include <QFileDialog>
-#include <QPainter>
-#include <QPixmap>
-#include <QtGlobal>
-
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-
-#include "timeline/widgets/FileItem.h"
-
-constexpr int MaxWidth           = 400;
-constexpr int Height             = 70;
-constexpr int IconRadius         = 22;
-constexpr int IconDiameter       = IconRadius * 2;
-constexpr int HorizontalPadding  = 12;
-constexpr int TextPadding        = 15;
-constexpr int DownloadIconRadius = IconRadius - 4;
-
-constexpr double VerticalPadding = Height - 2 * IconRadius;
-constexpr double IconYCenter     = Height / 2;
-constexpr double IconXCenter     = HorizontalPadding + IconRadius;
-
-void
-FileItem::init()
-{
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
-
-        icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
-
-        setFixedHeight(Height);
-}
-
-FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
-  : QWidget(parent)
-  , url_{QString::fromStdString(event.content.url)}
-  , text_{QString::fromStdString(event.content.body)}
-  , event_{event}
-{
-        readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
-
-        init();
-}
-
-FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        readableFileSize_ = utils::humanReadableFileSize(size);
-
-        init();
-}
-
-void
-FileItem::openUrl()
-{
-        if (url_.toString().isEmpty())
-                return;
-
-        auto urlToOpen = utils::mxcToHttp(
-          url_, QString::fromStdString(http::client()->server()), http::client()->port());
-
-        if (!QDesktopServices::openUrl(urlToOpen))
-                nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString());
-}
-
-QSize
-FileItem::sizeHint() const
-{
-        return QSize(MaxWidth, Height);
-}
-
-void
-FileItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->button() != Qt::LeftButton)
-                return;
-
-        auto point = event->pos();
-
-        // Click on the download icon.
-        if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter)
-              .contains(point)) {
-                filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_);
-
-                if (filenameToSave_.isEmpty())
-                        return;
-
-                auto proxy = std::make_shared<MediaProxy>();
-                connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded);
-
-                http::client()->download(
-                  url_.toString().toStdString(),
-                  [proxy = std::move(proxy), url = url_](const std::string &data,
-                                                         const std::string &,
-                                                         const std::string &,
-                                                         mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::ui()->warn("failed to retrieve m.file content: {}",
-                                                    url.toString().toStdString());
-                                  return;
-                          }
-
-                          emit proxy->fileDownloaded(QByteArray(data.data(), data.size()));
-                  });
-        } else {
-                openUrl();
-        }
-}
-
-void
-FileItem::fileDownloaded(const QByteArray &data)
-{
-        try {
-                QFile file(filenameToSave_);
-
-                if (!file.open(QIODevice::WriteOnly))
-                        return;
-
-                file.write(data);
-                file.close();
-        } catch (const std::exception &e) {
-                nhlog::ui()->warn("Error while saving file to: {}", e.what());
-        }
-}
-
-void
-FileItem::resizeEvent(QResizeEvent *event)
-{
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-        const int computedWidth = std::min(
-          fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth);
-#else
-        const int computedWidth =
-          std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding,
-                   (double)MaxWidth);
-#endif
-        resize(computedWidth, Height);
-
-        event->accept();
-}
-
-void
-FileItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-
-        QFont font;
-        font.setWeight(QFont::Medium);
-
-        QFontMetrics fm(font);
-
-        QPainterPath path;
-        path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(path, backgroundColor_);
-        painter.drawPath(path);
-
-        QPainterPath circle;
-        circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(circle, iconColor_);
-        painter.drawPath(circle);
-
-        icon_.paint(&painter,
-                    QRect(IconXCenter - DownloadIconRadius / 2,
-                          IconYCenter - DownloadIconRadius / 2,
-                          DownloadIconRadius,
-                          DownloadIconRadius),
-                    Qt::AlignCenter,
-                    QIcon::Normal);
-
-        const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding;
-        const int textStartY = VerticalPadding + fm.ascent() / 2;
-
-        // Draw the filename.
-        QString elidedText = fm.elidedText(
-          text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius);
-
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY), elidedText);
-
-        // Draw the filesize.
-        font.setWeight(QFont::Normal);
-        painter.setFont(font);
-        painter.setPen(QPen(textColor_));
-        painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_);
-}
diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h
deleted file mode 100644
index d63cce88..00000000
--- a/src/timeline/widgets/FileItem.h
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QIcon>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-class FileItem : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-
-public:
-        FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event,
-                 QWidget *parent = nullptr);
-
-        FileItem(const QString &url,
-                 const QString &filename,
-                 uint64_t size,
-                 QWidget *parent = nullptr);
-
-        QSize sizeHint() const override;
-
-        void setTextColor(const QColor &color) { textColor_ = color; }
-        void setIconColor(const QColor &color) { iconColor_ = color; }
-        void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
-
-        QColor textColor() const { return textColor_; }
-        QColor iconColor() const { return iconColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-
-private slots:
-        void fileDownloaded(const QByteArray &data);
-
-private:
-        void openUrl();
-        void init();
-
-        QUrl url_;
-        QString text_;
-        QString readableFileSize_;
-        QString filenameToSave_;
-
-        mtx::events::RoomEvent<mtx::events::msg::File> event_;
-
-        QIcon icon_;
-
-        QColor textColor_       = QColor("white");
-        QColor iconColor_       = QColor("#38A3D8");
-        QColor backgroundColor_ = QColor("#333");
-};
diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp
deleted file mode 100644
index 26c569d7..00000000
--- a/src/timeline/widgets/ImageItem.cpp
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFileDialog>
-#include <QFileInfo>
-#include <QPainter>
-#include <QPixmap>
-#include <QUuid>
-#include <QtGlobal>
-
-#include "Config.h"
-#include "ImageItem.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-#include "dialogs/ImageOverlay.h"
-
-void
-ImageItem::downloadMedia(const QUrl &url)
-{
-        auto proxy = std::make_shared<MediaProxy>();
-        connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage);
-
-        http::client()->download(url.toString().toStdString(),
-                                 [proxy = std::move(proxy), url](const std::string &data,
-                                                                 const std::string &,
-                                                                 const std::string &,
-                                                                 mtx::http::RequestErr err) {
-                                         if (err) {
-                                                 nhlog::net()->warn(
-                                                   "failed to retrieve image {}: {} {}",
-                                                   url.toString().toStdString(),
-                                                   err->matrix_error.error,
-                                                   static_cast<int>(err->status_code));
-                                                 return;
-                                         }
-
-                                         QPixmap img;
-                                         img.loadFromData(QByteArray(data.data(), data.size()));
-
-                                         emit proxy->imageDownloaded(img);
-                                 });
-}
-
-void
-ImageItem::saveImage(const QString &filename, const QByteArray &data)
-{
-        try {
-                QFile file(filename);
-
-                if (!file.open(QIODevice::WriteOnly))
-                        return;
-
-                file.write(data);
-                file.close();
-        } catch (const std::exception &e) {
-                nhlog::ui()->warn("Error while saving file to: {}", e.what());
-        }
-}
-
-void
-ImageItem::init()
-{
-        setMouseTracking(true);
-        setCursor(Qt::PointingHandCursor);
-        setAttribute(Qt::WA_Hover, true);
-
-        downloadMedia(url_);
-}
-
-ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
-  : QWidget(parent)
-  , event_{event}
-{
-        url_  = QString::fromStdString(event.content.url);
-        text_ = QString::fromStdString(event.content.body);
-
-        init();
-}
-
-ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        Q_UNUSED(size);
-        init();
-}
-
-void
-ImageItem::openUrl()
-{
-        if (url_.toString().isEmpty())
-                return;
-
-        auto urlToOpen = utils::mxcToHttp(
-          url_, QString::fromStdString(http::client()->server()), http::client()->port());
-
-        if (!QDesktopServices::openUrl(urlToOpen))
-                nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString());
-}
-
-QSize
-ImageItem::sizeHint() const
-{
-        if (image_.isNull())
-                return QSize(max_width_, bottom_height_);
-
-        return QSize(width_, height_);
-}
-
-void
-ImageItem::setImage(const QPixmap &image)
-{
-        image_        = image;
-        scaled_image_ = utils::scaleDown(max_width_, max_height_, image_);
-
-        width_  = scaled_image_.width();
-        height_ = scaled_image_.height();
-
-        setFixedSize(width_, height_);
-        update();
-}
-
-void
-ImageItem::mousePressEvent(QMouseEvent *event)
-{
-        if (!isInteractive_) {
-                event->accept();
-                return;
-        }
-
-        if (event->button() != Qt::LeftButton)
-                return;
-
-        if (image_.isNull()) {
-                openUrl();
-                return;
-        }
-
-        if (textRegion_.contains(event->pos())) {
-                openUrl();
-        } else {
-                auto imgDialog = new dialogs::ImageOverlay(image_);
-                imgDialog->show();
-                connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs);
-        }
-}
-
-void
-ImageItem::resizeEvent(QResizeEvent *event)
-{
-        if (!image_)
-                return QWidget::resizeEvent(event);
-
-        scaled_image_ = utils::scaleDown(max_width_, max_height_, image_);
-
-        width_  = scaled_image_.width();
-        height_ = scaled_image_.height();
-
-        setFixedSize(width_, height_);
-}
-
-void
-ImageItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter painter(this);
-        painter.setRenderHint(QPainter::Antialiasing);
-
-        QFont font;
-
-        QFontMetrics metrics(font);
-        const int fontHeight = metrics.height() + metrics.ascent();
-
-        if (image_.isNull()) {
-                QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-                setFixedSize(metrics.width(elidedText), fontHeight);
-#else
-                setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight);
-#endif
-                painter.setFont(font);
-                painter.setPen(QPen(QColor(66, 133, 244)));
-                painter.drawText(QPoint(0, fontHeight / 2), elidedText);
-
-                return;
-        }
-
-        imageRegion_ = QRectF(0, 0, width_, height_);
-
-        QPainterPath path;
-        path.addRoundedRect(imageRegion_, 5, 5);
-
-        painter.setPen(Qt::NoPen);
-        painter.fillPath(path, scaled_image_);
-        painter.drawPath(path);
-
-        // Bottom text section
-        if (isInteractive_ && underMouse()) {
-                const int textBoxHeight = fontHeight / 2 + 6;
-
-                textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight);
-
-                QPainterPath textPath;
-                textPath.addRoundedRect(textRegion_, 0, 0);
-
-                painter.fillPath(textPath, QColor(40, 40, 40, 140));
-
-                QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10);
-
-                font.setWeight(QFont::Medium);
-                painter.setFont(font);
-                painter.setPen(QPen(QColor(Qt::white)));
-
-                textRegion_.adjust(5, 0, 5, 0);
-                painter.drawText(textRegion_, Qt::AlignVCenter, elidedText);
-        }
-}
-
-void
-ImageItem::saveAs()
-{
-        auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_);
-
-        if (filename.isEmpty())
-                return;
-
-        const auto url = url_.toString().toStdString();
-
-        auto proxy = std::make_shared<MediaProxy>();
-        connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage);
-
-        http::client()->download(
-          url,
-          [proxy = std::move(proxy), filename, url](const std::string &data,
-                                                    const std::string &,
-                                                    const std::string &,
-                                                    mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to retrieve image {}: {} {}",
-                                             url,
-                                             err->matrix_error.error,
-                                             static_cast<int>(err->status_code));
-                          return;
-                  }
-
-                  emit proxy->imageSaved(filename, QByteArray(data.data(), data.size()));
-          });
-}
diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h
deleted file mode 100644
index 65bd962d..00000000
--- a/src/timeline/widgets/ImageItem.h
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-namespace dialogs {
-class ImageOverlay;
-}
-
-class ImageItem : public QWidget
-{
-        Q_OBJECT
-public:
-        ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
-                  QWidget *parent = nullptr);
-
-        ImageItem(const QString &url,
-                  const QString &filename,
-                  uint64_t size,
-                  QWidget *parent = nullptr);
-
-        QSize sizeHint() const override;
-
-public slots:
-        //! Show a save as dialog for the image.
-        void saveAs();
-        void setImage(const QPixmap &image);
-        void saveImage(const QString &filename, const QByteArray &data);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void mousePressEvent(QMouseEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-
-        //! Whether the user can interact with the displayed image.
-        bool isInteractive_ = true;
-
-private:
-        void init();
-        void openUrl();
-        void downloadMedia(const QUrl &url);
-
-        int max_width_  = 500;
-        int max_height_ = 300;
-
-        int width_;
-        int height_;
-
-        QPixmap scaled_image_;
-        QPixmap image_;
-
-        QUrl url_;
-        QString text_;
-
-        int bottom_height_ = 30;
-
-        QRectF textRegion_;
-        QRectF imageRegion_;
-
-        mtx::events::RoomEvent<mtx::events::msg::Image> event_;
-};
-
-class StickerItem : public ImageItem
-{
-        Q_OBJECT
-
-public:
-        StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr)
-          : ImageItem{QString::fromStdString(event.content.url),
-                      QString::fromStdString(event.content.body),
-                      event.content.info.size,
-                      parent}
-          , event_{event}
-        {
-                isInteractive_ = false;
-                setCursor(Qt::ArrowCursor);
-                setMouseTracking(false);
-                setAttribute(Qt::WA_Hover, false);
-        }
-
-private:
-        mtx::events::Sticker event_;
-};
diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp
deleted file mode 100644
index 4b5dc022..00000000
--- a/src/timeline/widgets/VideoItem.cpp
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <QLabel>
-#include <QVBoxLayout>
-
-#include "Config.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-#include "timeline/widgets/VideoItem.h"
-
-void
-VideoItem::init()
-{
-        url_ = utils::mxcToHttp(
-          url_, QString::fromStdString(http::client()->server()), http::client()->port());
-}
-
-VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)
-  : QWidget(parent)
-  , url_{QString::fromStdString(event.content.url)}
-  , text_{QString::fromStdString(event.content.body)}
-  , event_{event}
-{
-        readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
-
-        init();
-
-        auto layout = new QVBoxLayout(this);
-        layout->setMargin(0);
-        layout->setSpacing(0);
-
-        QString link = QString("<a href=%1>%2</a>").arg(url_.toString()).arg(text_);
-
-        label_ = new QLabel(link, this);
-        label_->setMargin(0);
-        label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
-        label_->setOpenExternalLinks(true);
-
-        layout->addWidget(label_);
-}
-
-VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
-  : QWidget(parent)
-  , url_{url}
-  , text_{filename}
-{
-        readableFileSize_ = utils::humanReadableFileSize(size);
-
-        init();
-}
diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h
deleted file mode 100644
index 26fa1c35..00000000
--- a/src/timeline/widgets/VideoItem.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QLabel>
-#include <QSharedPointer>
-#include <QUrl>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-class VideoItem : public QWidget
-{
-        Q_OBJECT
-
-public:
-        VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
-                  QWidget *parent = nullptr);
-
-        VideoItem(const QString &url,
-                  const QString &filename,
-                  uint64_t size,
-                  QWidget *parent = nullptr);
-
-private:
-        void init();
-
-        QUrl url_;
-        QString text_;
-        QString readableFileSize_;
-
-        QLabel *label_;
-
-        mtx::events::RoomEvent<mtx::events::msg::Video> event_;
-};