summary refs log tree commit diff
path: root/src/timeline
diff options
context:
space:
mode:
authorAkhil Nair <targetakhil@gmail.com>2021-02-14 00:04:27 -0500
committerGitHub <noreply@github.com>2021-02-14 00:04:27 -0500
commit84005404289da6c2be9f524b7b8c70a1be7ad565 (patch)
tree1de2d0fd5fb1aab4c5ea4604567edb0844ed3b26 /src/timeline
parentreplaced with togglebutton using qtquickcontrols2 (diff)
parentAdd double tap to reply feature (diff)
downloadnheko-84005404289da6c2be9f524b7b8c70a1be7ad565.tar.xz
Merge branch 'master' into room_settings_qml
Diffstat (limited to 'src/timeline')
-rw-r--r--src/timeline/DelegateChooser.cpp4
-rw-r--r--src/timeline/EventStore.cpp104
-rw-r--r--src/timeline/EventStore.h4
-rw-r--r--src/timeline/InputBar.cpp58
-rw-r--r--src/timeline/InputBar.h2
-rw-r--r--src/timeline/TimelineModel.cpp99
-rw-r--r--src/timeline/TimelineModel.h15
-rw-r--r--src/timeline/TimelineViewManager.cpp20
-rw-r--r--src/timeline/TimelineViewManager.h2
9 files changed, 253 insertions, 55 deletions
diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp
index 1f5fae7e..8598fa77 100644
--- a/src/timeline/DelegateChooser.cpp
+++ b/src/timeline/DelegateChooser.cpp
@@ -123,10 +123,6 @@ DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status)
                 }
 
                 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);
                 emit chooser.childChanged();
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index be4bc09e..94d43a83 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -293,16 +293,16 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
         }
 
         for (const auto &event : events.events) {
-                std::string relates_to;
+                std::set<std::string> relates_to;
                 if (auto redaction =
                       std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(
                         &event)) {
                         // fixup reactions
                         auto redacted = events_by_id_.object({room_id_, redaction->redacts});
                         if (redacted) {
-                                auto id = mtx::accessors::relates_to_event_id(*redacted);
-                                if (!id.empty()) {
-                                        auto idx = idToIndex(id);
+                                auto id = mtx::accessors::relations(*redacted);
+                                if (id.annotates()) {
+                                        auto idx = idToIndex(id.annotates()->event_id);
                                         if (idx) {
                                                 events_by_id_.remove(
                                                   {room_id_, redaction->redacts});
@@ -312,20 +312,17 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
                                 }
                         }
 
-                        relates_to = redaction->redacts;
-                } else if (auto reaction =
-                             std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
-                               &event)) {
-                        relates_to = reaction->content.relates_to.event_id;
+                        relates_to.insert(redaction->redacts);
                 } else {
-                        relates_to = mtx::accessors::in_reply_to_event(event);
+                        for (const auto &r : mtx::accessors::relations(event).relations)
+                                relates_to.insert(r.event_id);
                 }
 
-                if (!relates_to.empty()) {
-                        auto idx = cache::client()->getTimelineIndex(room_id_, relates_to);
+                for (const auto &relates_to_id : relates_to) {
+                        auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id);
                         if (idx) {
-                                events_by_id_.remove({room_id_, relates_to});
-                                decryptedEvents_.remove({room_id_, relates_to});
+                                events_by_id_.remove({room_id_, relates_to_id});
+                                decryptedEvents_.remove({room_id_, relates_to_id});
                                 events_.remove({room_id_, *idx});
                                 emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
                         }
@@ -408,6 +405,52 @@ EventStore::handle_room_verification(mtx::events::collections::TimelineEvents ev
           event);
 }
 
+std::vector<mtx::events::collections::TimelineEvents>
+EventStore::edits(const std::string &event_id)
+{
+        auto event_ids = cache::client()->relatedEvents(room_id_, event_id);
+
+        auto original_event = get(event_id, "", false, false);
+        if (!original_event)
+                return {};
+
+        auto original_sender    = mtx::accessors::sender(*original_event);
+        auto original_relations = mtx::accessors::relations(*original_event);
+
+        std::vector<mtx::events::collections::TimelineEvents> edits;
+        for (const auto &id : event_ids) {
+                auto related_event = get(id, event_id, false, false);
+                if (!related_event)
+                        continue;
+
+                auto related_ev = *related_event;
+
+                auto edit_rel = mtx::accessors::relations(related_ev);
+                if (edit_rel.replaces() == event_id &&
+                    original_sender == mtx::accessors::sender(related_ev)) {
+                        if (edit_rel.synthesized && original_relations.reply_to() &&
+                            !edit_rel.reply_to()) {
+                                edit_rel.relations.push_back(
+                                  {mtx::common::RelationType::InReplyTo,
+                                   original_relations.reply_to().value()});
+                                mtx::accessors::set_relations(related_ev, std::move(edit_rel));
+                        }
+                        edits.push_back(std::move(related_ev));
+                }
+        }
+
+        auto c = cache::client();
+        std::sort(edits.begin(),
+                  edits.end(),
+                  [this, c](const mtx::events::collections::TimelineEvents &a,
+                            const mtx::events::collections::TimelineEvents &b) {
+                          return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) <
+                                 c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b));
+                  });
+
+        return edits;
+}
+
 QVariantList
 EventStore::reactions(const std::string &event_id)
 {
@@ -430,13 +473,14 @@ EventStore::reactions(const std::string &event_id)
 
                 if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
                       related_event);
-                    reaction && reaction->content.relates_to.key) {
-                        auto &agg = aggregation[reaction->content.relates_to.key.value()];
+                    reaction && reaction->content.relations.annotates() &&
+                    reaction->content.relations.annotates()->key) {
+                        auto key  = reaction->content.relations.annotates()->key.value();
+                        auto &agg = aggregation[key];
 
                         if (agg.count == 0) {
                                 Reaction temp{};
-                                temp.key_ =
-                                  QString::fromStdString(reaction->content.relates_to.key.value());
+                                temp.key_ = QString::fromStdString(key);
                                 reactions.push_back(temp);
                         }
 
@@ -489,7 +533,13 @@ EventStore::get(int idx, bool decrypt)
                 if (!event_id)
                         return nullptr;
 
-                auto event = cache::client()->getEvent(room_id_, *event_id);
+                std::optional<mtx::events::collections::TimelineEvent> event;
+                auto edits_ = edits(*event_id);
+                if (edits_.empty())
+                        event = cache::client()->getEvent(room_id_, *event_id);
+                else
+                        event = {edits_.back()};
+
                 if (!event)
                         return nullptr;
                 else
@@ -691,8 +741,7 @@ EventStore::decryptEvent(const IdIndex &idx,
         body["unsigned"]         = e.unsigned_data;
 
         // relations are unencrypted in content...
-        if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
-                body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
+        mtx::common::add_relations(body["content"], e.content.relations);
 
         json event_array = json::array();
         event_array.push_back(body);
@@ -717,7 +766,7 @@ EventStore::decryptEvent(const IdIndex &idx,
 }
 
 mtx::events::collections::TimelineEvents *
-EventStore::get(std::string_view id, std::string_view related_to, bool decrypt)
+EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, bool resolve_edits)
 {
         if (this->thread() != QThread::currentThread())
                 nhlog::db()->warn("{} called from a different thread!", __func__);
@@ -725,7 +774,16 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt)
         if (id.empty())
                 return nullptr;
 
-        IdIndex index{room_id_, std::string(id.data(), id.size())};
+        IdIndex index{room_id_, std::string(id)};
+        if (resolve_edits) {
+                auto edits_ = edits(index.id);
+                if (!edits_.empty()) {
+                        index.id = mtx::accessors::event_id(edits_.back());
+                        auto event_ptr =
+                          new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
+                        events_by_id_.insert(index, event_ptr);
+                }
+        }
 
         auto event_ptr = events_by_id_.object(index);
         if (!event_ptr) {
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index f8eff9a9..ced7bdc0 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -66,7 +66,8 @@ public:
         // relatedFetched event
         mtx::events::collections::TimelineEvents *get(std::string_view id,
                                                       std::string_view related_to,
-                                                      bool decrypt = true);
+                                                      bool decrypt       = true,
+                                                      bool resolve_edits = true);
         // always returns a proper event as long as the idx is valid
         mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
 
@@ -110,6 +111,7 @@ public slots:
         void clearTimeline();
 
 private:
+        std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
         mtx::events::collections::TimelineEvents *decryptEvent(
           const IdIndex &idx,
           const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index b31c1f76..08cbd15b 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -268,7 +268,18 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
                         text.format = "org.matrix.custom.html";
         }
 
-        if (!room->reply().isEmpty()) {
+        if (!room->edit().isEmpty()) {
+                if (!room->reply().isEmpty()) {
+                        text.relations.relations.push_back(
+                          {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+                        room->resetReply();
+                }
+
+                text.relations.relations.push_back(
+                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
+                room->resetEdit();
+
+        } else if (!room->reply().isEmpty()) {
                 auto related = room->relatedInfo(room->reply());
 
                 QString body;
@@ -294,7 +305,8 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
                         text.formatted_body =
                           utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString();
 
-                text.relates_to.in_reply_to.event_id = related.related_event;
+                text.relations.relations.push_back(
+                  {mtx::common::RelationType::InReplyTo, related.related_event});
                 room->resetReply();
         }
 
@@ -316,9 +328,15 @@ InputBar::emote(QString msg)
         }
 
         if (!room->reply().isEmpty()) {
-                emote.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                emote.relations.relations.push_back(
+                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
                 room->resetReply();
         }
+        if (!room->edit().isEmpty()) {
+                emote.relations.relations.push_back(
+                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
+                room->resetEdit();
+        }
 
         room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
 }
@@ -346,9 +364,15 @@ InputBar::image(const QString &filename,
                 image.url = url.toStdString();
 
         if (!room->reply().isEmpty()) {
-                image.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                image.relations.relations.push_back(
+                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
                 room->resetReply();
         }
+        if (!room->edit().isEmpty()) {
+                image.relations.relations.push_back(
+                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
+                room->resetEdit();
+        }
 
         room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
 }
@@ -371,9 +395,15 @@ InputBar::file(const QString &filename,
                 file.url = url.toStdString();
 
         if (!room->reply().isEmpty()) {
-                file.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                file.relations.relations.push_back(
+                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
                 room->resetReply();
         }
+        if (!room->edit().isEmpty()) {
+                file.relations.relations.push_back(
+                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
+                room->resetEdit();
+        }
 
         room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
 }
@@ -397,9 +427,15 @@ InputBar::audio(const QString &filename,
                 audio.url = url.toStdString();
 
         if (!room->reply().isEmpty()) {
-                audio.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                audio.relations.relations.push_back(
+                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
                 room->resetReply();
         }
+        if (!room->edit().isEmpty()) {
+                audio.relations.relations.push_back(
+                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
+                room->resetEdit();
+        }
 
         room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
 }
@@ -422,9 +458,15 @@ InputBar::video(const QString &filename,
                 video.url = url.toStdString();
 
         if (!room->reply().isEmpty()) {
-                video.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                video.relations.relations.push_back(
+                  {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
                 room->resetReply();
         }
+        if (!room->edit().isEmpty()) {
+                video.relations.relations.push_back(
+                  {mtx::common::RelationType::Replace, room->edit().toStdString()});
+                room->resetEdit();
+        }
 
         room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
 }
@@ -518,6 +560,8 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList &
           [this](const QByteArray data, const QString &mime, const QString &fn) {
                   setUploading(true);
 
+                  setText("");
+
                   auto payload = std::string(data.data(), data.size());
                   std::optional<mtx::crypto::EncryptedFile> encryptedFile;
                   if (cache::isRoomEncrypted(room->roomId().toStdString())) {
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index f173bbc0..696a0dd9 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -41,6 +41,7 @@ public slots:
         QString text() const;
         QString previousText();
         QString nextText();
+        void setText(QString newText) { emit textChanged(newText); }
 
         void send();
         void paste(bool fromMouse);
@@ -58,6 +59,7 @@ private slots:
 
 signals:
         void insertText(QString text);
+        void textChanged(QString newText);
         void uploadingChanged(bool value);
 
 private:
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index efeba146..6caac132 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -288,6 +288,8 @@ TimelineModel::roleNames() const
           {ProportionalHeight, "proportionalHeight"},
           {Id, "id"},
           {State, "state"},
+          {IsEdited, "isEdited"},
+          {IsEditable, "isEditable"},
           {IsEncrypted, "isEncrypted"},
           {IsRoomEncrypted, "isRoomEncrypted"},
           {ReplyTo, "replyTo"},
@@ -360,7 +362,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                 const static QRegularExpression replyFallback(
                   "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
 
-                bool isReply = !in_reply_to_event(event).empty();
+                bool isReply = relations(event).reply_to().has_value();
 
                 auto formattedBody_ = QString::fromStdString(formatted_body(event));
                 if (formattedBody_.isEmpty()) {
@@ -409,8 +411,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
 
                 return QVariant(prop > 0 ? prop : 1.);
         }
-        case Id:
-                return QVariant(QString::fromStdString(event_id(event)));
+        case Id: {
+                if (auto replaces = relations(event).replaces())
+                        return QVariant(QString::fromStdString(replaces.value()));
+                else
+                        return QVariant(QString::fromStdString(event_id(event)));
+        }
         case State: {
                 auto id             = QString::fromStdString(event_id(event));
                 auto containsOthers = [](const auto &vec) {
@@ -430,6 +436,11 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                 else
                         return qml_mtx_events::Received;
         }
+        case IsEdited:
+                return QVariant(relations(event).replaces().has_value());
+        case IsEditable:
+                return QVariant(!is_state_event(event) && mtx::accessors::sender(event) ==
+                                                            http::client()->user_id().to_string());
         case IsEncrypted: {
                 auto id              = event_id(event);
                 auto encrypted_event = events.get(id, id, false);
@@ -442,9 +453,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                 return cache::isRoomEncrypted(room_id_.toStdString());
         }
         case ReplyTo:
-                return QVariant(QString::fromStdString(in_reply_to_event(event)));
+                return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
         case Reactions: {
-                auto id = event_id(event);
+                auto id = relations(event).replaces().value_or(event_id(event));
                 return QVariant::fromValue(events.reactions(id));
         }
         case RoomId:
@@ -724,15 +735,30 @@ TimelineModel::updateLastMessage()
 void
 TimelineModel::setCurrentIndex(int index)
 {
-        if (!ChatPage::instance()->isActiveWindow())
-                return;
-
         auto oldIndex = idToIndex(currentId);
         currentId     = indexToId(index);
-        emit currentIndexChanged(index);
+        if (index != oldIndex)
+                emit currentIndexChanged(index);
+
+        if (!ChatPage::instance()->isActiveWindow())
+                return;
 
-        if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m")) {
-                readEvent(currentId.toStdString());
+        if (!currentId.startsWith("m")) {
+                auto oldReadIndex =
+                  cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString());
+                auto nextEventIndexAndId =
+                  cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString());
+
+                if (nextEventIndexAndId &&
+                    (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
+                        readEvent(nextEventIndexAndId->second);
+                        currentReadId = QString::fromStdString(nextEventIndexAndId->second);
+
+                        nhlog::net()->info("Marked as read {}, index {}, oldReadIndex {}",
+                                           nextEventIndexAndId->second,
+                                           nextEventIndexAndId->first,
+                                           *oldReadIndex);
+                }
         }
 }
 
@@ -821,6 +847,12 @@ TimelineModel::replyAction(QString id)
         setReply(id);
 }
 
+void
+TimelineModel::editAction(QString id)
+{
+        setEdit(id);
+}
+
 RelatedInfo
 TimelineModel::relatedInfo(QString id)
 {
@@ -1509,6 +1541,51 @@ TimelineModel::formatMemberEvent(QString id)
         return rendered;
 }
 
+void
+TimelineModel::setEdit(QString newEdit)
+{
+        if (edit_.startsWith('m'))
+                return;
+
+        if (edit_ != newEdit) {
+                auto ev = events.get(newEdit.toStdString(), "");
+                if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
+                        auto e = *ev;
+                        setReply(QString::fromStdString(
+                          mtx::accessors::relations(e).reply_to().value_or("")));
+
+                        auto msgType = mtx::accessors::msg_type(e);
+                        if (msgType == mtx::events::MessageType::Text ||
+                            msgType == mtx::events::MessageType::Notice) {
+                                input()->setText(relatedInfo(newEdit).quoted_body);
+                        } else if (msgType == mtx::events::MessageType::Emote) {
+                                input()->setText("/me " + relatedInfo(newEdit).quoted_body);
+                        } else {
+                                input()->setText("");
+                        }
+
+                        edit_ = newEdit;
+                } else {
+                        resetReply();
+
+                        input()->setText("");
+                        edit_ = "";
+                }
+                emit editChanged(edit_);
+        }
+}
+
+void
+TimelineModel::resetEdit()
+{
+        if (!edit_.isEmpty()) {
+                edit_ = "";
+                emit editChanged(edit_);
+                input()->setText("");
+                resetReply();
+        }
+}
+
 QString
 TimelineModel::roomName() const
 {
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index df067fd4..5f599741 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -146,6 +146,7 @@ class TimelineModel : public QAbstractListModel
         Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY
                      typingUsersChanged)
         Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply)
+        Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit)
         Q_PROPERTY(
           bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
         Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
@@ -182,6 +183,8 @@ public:
                 ProportionalHeight,
                 Id,
                 State,
+                IsEdited,
+                IsEditable,
                 IsEncrypted,
                 IsRoomEncrypted,
                 ReplyTo,
@@ -215,6 +218,7 @@ public:
         Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
         Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
         Q_INVOKABLE void openRoomSettings();
+        Q_INVOKABLE void editAction(QString id);
         Q_INVOKABLE void replyAction(QString id);
         Q_INVOKABLE void readReceiptsAction(QString id) const;
         Q_INVOKABLE void redactEvent(QString id);
@@ -258,6 +262,9 @@ public slots:
         QString reply() const { return reply_; }
         void setReply(QString newReply)
         {
+                if (edit_.startsWith('m'))
+                        return;
+
                 if (reply_ != newReply) {
                         reply_ = newReply;
                         emit replyChanged(reply_);
@@ -270,6 +277,9 @@ public slots:
                         emit replyChanged(reply_);
                 }
         }
+        QString edit() const { return edit_; }
+        void setEdit(QString newEdit);
+        void resetEdit();
         void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
         void clearTimeline() { events.clearTimeline(); }
         void receivedSessionKey(const std::string &session_key)
@@ -294,6 +304,7 @@ signals:
         void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
         void typingUsersChanged(std::vector<QString> users);
         void replyChanged(QString reply);
+        void editChanged(QString reply);
         void paginationInProgressChanged(const bool);
         void newCallEvent(const mtx::events::collections::TimelineEvents &event);
 
@@ -324,8 +335,8 @@ private:
         bool decryptDescription     = true;
         bool m_paginationInProgress = false;
 
-        QString currentId;
-        QString reply_;
+        QString currentId, currentReadId;
+        QString reply_, edit_;
         std::vector<QString> typingUsers_;
 
         TimelineViewManager *manager_;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 0ed680f8..f2e6d571 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -21,6 +21,7 @@
 #include "dialogs/ImageOverlay.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "ui/NhekoCursorShape.h"
 #include "ui/NhekoDropArea.h"
 
 #include <iostream> //only for debugging
@@ -118,6 +119,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
         qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
         qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
+        qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
         qmlRegisterUncreatableType<DeviceVerificationFlow>(
           "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
         qmlRegisterUncreatableType<UserProfile>(
@@ -176,10 +178,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         view->setResizeMode(QQuickWidget::SizeRootObjectToView);
         container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
-        view->quickWindow()->setTextRenderType(QQuickWindow::NativeTextRendering);
-#endif
-
         connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
                 nhlog::ui()->debug("Status changed to {}", status);
         });
@@ -508,9 +506,11 @@ TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QSt
         // If selfReactedEvent is empty, that means we haven't previously reacted
         if (selfReactedEvent.isEmpty()) {
                 mtx::events::msg::Reaction reaction;
-                reaction.relates_to.rel_type = mtx::common::RelationType::Annotation;
-                reaction.relates_to.event_id = reactedEvent.toStdString();
-                reaction.relates_to.key      = reactionKey.toStdString();
+                mtx::common::Relation rel;
+                rel.rel_type = mtx::common::RelationType::Annotation;
+                rel.event_id = reactedEvent.toStdString();
+                rel.key      = reactionKey.toStdString();
+                reaction.relations.relations.push_back(rel);
 
                 timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
                 // Otherwise, we have previously reacted and the reaction should be redacted
@@ -546,3 +546,9 @@ TimelineViewManager::queueCallMessage(const QString &roomid,
 {
         models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
 }
+
+void
+TimelineViewManager::focusMessageInput()
+{
+        emit focusInput();
+}
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 3e58bb43..61fce574 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -66,6 +66,7 @@ public:
 
         Q_INVOKABLE void openLink(QString link) const;
 
+        Q_INVOKABLE void focusMessageInput();
         Q_INVOKABLE void openInviteUsersDialog();
         Q_INVOKABLE void openMemberListDialog() const;
         Q_INVOKABLE void openLeaveRoomDialog() const;
@@ -86,6 +87,7 @@ signals:
         void showRoomList();
         void narrowViewChanged();
         void focusChanged();
+        void focusInput();
 
 public slots:
         void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);