summary refs log tree commit diff
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2024-03-08 18:43:59 +0100
committerNicolas Werner <nicolas.werner@hotmail.de>2024-03-08 18:45:18 +0100
commit7c2a152cfbc2197989f7d722deb961ac80269019 (patch)
tree1ff5bc7c938ceb2242a0d769f9f64ef5e980f683
parentFix buttons vanishing on the kde themes in the settings page (diff)
downloadnheko-7c2a152cfbc2197989f7d722deb961ac80269019.tar.xz
Add support for intentional mentions
This is still a bit flaky around when to remove a mention, but it should
work in most cases. Might add a toggle in the future to disable these
though.
-rw-r--r--CMakeLists.txt2
-rw-r--r--im.nheko.Nheko.yaml2
-rw-r--r--resources/qml/Completer.qml7
-rw-r--r--resources/qml/MessageInput.qml11
-rw-r--r--resources/qml/MessageInputWarning.qml23
-rw-r--r--resources/qml/TimelineDefaultMessageStyle.qml24
-rw-r--r--resources/qml/TimelineView.qml13
-rw-r--r--resources/qml/dialogs/RoomSettingsDialog.qml2
-rw-r--r--src/ChatPage.cpp3
-rw-r--r--src/EventAccessors.cpp17
-rw-r--r--src/EventAccessors.h2
-rw-r--r--src/timeline/InputBar.cpp119
-rw-r--r--src/timeline/InputBar.h43
-rw-r--r--src/timeline/TimelineModel.cpp27
-rw-r--r--src/timeline/TimelineModel.h2
15 files changed, 244 insertions, 53 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fc04e3a0..6b4bf52f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -603,7 +603,7 @@ if(USE_BUNDLED_MTXCLIENT)
     FetchContent_Declare(
         MatrixClient
             GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-            GIT_TAG        03bb6fbd665260faec0148b5bb0bfe484e88581a
+            GIT_TAG        188ecb899744e55842c1debaa4597cdc5184be8a
     )
     set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
     set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
diff --git a/im.nheko.Nheko.yaml b/im.nheko.Nheko.yaml
index 917980a4..a2141baa 100644
--- a/im.nheko.Nheko.yaml
+++ b/im.nheko.Nheko.yaml
@@ -223,7 +223,7 @@ modules:
     buildsystem: cmake-ninja
     name: mtxclient
     sources:
-      - commit: 03bb6fbd665260faec0148b5bb0bfe484e88581a
+      - commit: 188ecb899744e55842c1debaa4597cdc5184be8a
         #tag: v0.9.2
         type: git
         url: https://github.com/Nheko-Reborn/mtxclient.git
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 965789bc..c6fea98e 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -42,6 +42,13 @@ Control {
         else
             return null;
     }
+    function currentUserid() {
+        if (popup.completerName == "user") {
+            return listView.itemAtIndex(currentIndex).modelData.userid;
+        } else {
+            return "";
+        }
+    }
     function down() {
         if (bottomToTop)
             up_();
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 4396f1d3..8b6af57a 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -114,6 +114,10 @@ Rectangle {
                 function insertCompletion(completion) {
                     messageInput.remove(completerTriggeredAt, cursorPosition);
                     messageInput.insert(cursorPosition, completion);
+                    let userid = completer.currentUserid();
+                    if (userid) {
+                        room.input.addMention(userid, completion);
+                    }
                 }
                 function openCompleter(pos, type) {
                     if (popup.opened)
@@ -176,10 +180,17 @@ Rectangle {
                     } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                         if (popup.opened) {
                             var currentCompletion = completer.currentCompletion();
+                            let userid = completer.currentUserid();
+
                             completer.completerName = "";
                             popup.close();
+
                             if (currentCompletion) {
                                 messageInput.insertCompletion(currentCompletion);
+                                if (userid) {
+                                    console.log(userid);
+                                    room.input.addMention(userid, currentCompletion);
+                                }
                                 event.accepted = true;
                                 return;
                             }
diff --git a/resources/qml/MessageInputWarning.qml b/resources/qml/MessageInputWarning.qml
index 4d5578b3..82658f58 100644
--- a/resources/qml/MessageInputWarning.qml
+++ b/resources/qml/MessageInputWarning.qml
@@ -12,6 +12,9 @@ Rectangle {
 
     property color bubbleColor: Nheko.theme.error
     required property string text
+    property bool showRemove: false
+
+    signal removeClicked();
 
     Layout.fillWidth: true
     color: palette.window // required to hide the timeline behind this warning
@@ -35,10 +38,30 @@ Rectangle {
             id: warningDisplay
 
             anchors.left: parent.left
+            anchors.right: parent.right
             anchors.margins: Nheko.paddingSmall
+            anchors.rightMargin: warningRoot.showRemove ? (Nheko.paddingSmall*3 + removeButton.width) : Nheko.paddingSmall
             anchors.verticalCenter: parent.verticalCenter
             text: warningRoot.text
             textFormat: Text.PlainText
         }
+
+        ImageButton {
+            id: removeButton
+
+            visible: warningRoot.showRemove
+
+            anchors.right: parent.right
+            anchors.margins: Nheko.paddingSmall
+            anchors.verticalCenter: parent.verticalCenter
+
+            image: ":/icons/icons/ui/dismiss.svg"
+            hoverEnabled: true
+            ToolTip.visible: hovered
+            ToolTip.text: qsTr("Don't mention them in this message")
+            onClicked: {
+                warningRoot.removeClicked();
+            }
+        }
     }
 }
diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml
index 55c62b02..34808323 100644
--- a/resources/qml/TimelineDefaultMessageStyle.qml
+++ b/resources/qml/TimelineDefaultMessageStyle.qml
@@ -128,18 +128,6 @@ TimelineEvent {
                 }
             }
         },
-        Rectangle {
-            anchors.top: gridContainer.top
-            anchors.left: gridContainer.left 
-            anchors.topMargin: -2
-            anchors.leftMargin: -2 + (stateEventSpacing.visible ? (stateEventSpacing.width + gridContainer.spacing) : 0)
-            color: "transparent"
-            border.color: Nheko.theme.red
-            border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0
-            radius: 4
-            height: contentColumn.implicitHeight + 4
-            width: contentColumn.implicitWidth + 4 + (wrapper.threadId ? (4 + gridContainer.spacing) : 0)
-        },
         Row {
             id: gridContainer
 
@@ -293,6 +281,18 @@ TimelineEvent {
                 onDoubleTapped: wrapper.room.reply = wrapper.eventId
             }
         },
+        Rectangle {
+            anchors.top: gridContainer.top
+            anchors.left: gridContainer.left 
+            anchors.topMargin: -2
+            anchors.leftMargin: -2 + (stateEventSpacing.visible ? (stateEventSpacing.width + gridContainer.spacing) : 0)
+            color: "transparent"
+            border.color: Nheko.theme.red
+            border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0
+            radius: 4
+            height: contentColumn.implicitHeight + 4
+            width: contentColumn.implicitWidth + 4 + (wrapper.threadId ? (4 + gridContainer.spacing) : 0)
+        },
             TimelineMetadata {
                 id: metadata
 
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 7bad53c4..085ca073 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -148,9 +148,16 @@ Item {
         }
         UploadBox {
         }
-        MessageInputWarning {
-            text: qsTr("You are about to notify the whole room")
-            visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
+        Repeater {
+            model: room ? room.input.mentions : null
+
+            MessageInputWarning {
+                required property string modelData
+                bubbleColor: modelData == "@room" ? Nheko.theme.error : Nheko.theme.orange
+                text: modelData == "@room" ? qsTr("You are about to notify the whole room") : qsTr("You will be mentioning %1").arg(modelData)
+                showRemove: true
+                onRemoveClicked: room.input.removeMention(modelData);
+            }
         }
         MessageInputWarning {
             text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "")
diff --git a/resources/qml/dialogs/RoomSettingsDialog.qml b/resources/qml/dialogs/RoomSettingsDialog.qml
index 9276a9d3..8e127567 100644
--- a/resources/qml/dialogs/RoomSettingsDialog.qml
+++ b/resources/qml/dialogs/RoomSettingsDialog.qml
@@ -273,7 +273,7 @@ ApplicationWindow {
                 ComboBox {
                     model: [qsTr("Muted"), qsTr("Mentions only"), qsTr("All messages")]
                     currentIndex: roomSettings.notifications
-                    onActivated: {
+                    onActivated: (index) => {
                         roomSettings.changeNotifications(index);
                     }
                     Layout.fillWidth: true
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index a75b7ef5..a43a190c 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -198,9 +198,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent)
             if (eventInDb) {
                 if (auto newRules =
                       std::get_if<mtx::events::AccountDataEvent<mtx::pushrules::GlobalRuleset>>(
-                        &*eventInDb))
+                        &*eventInDb)) {
                     pushrules =
                       std::make_unique<mtx::pushrules::PushRuleEvaluator>(newRules->content.global);
+                }
             }
         }
         if (pushrules) {
diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index ca730a75..a43d62c6 100644
--- a/src/EventAccessors.cpp
+++ b/src/EventAccessors.cpp
@@ -241,6 +241,18 @@ struct EventRelations
     }
 };
 
+struct EventMentions
+{
+    template<class T>
+    std::optional<mtx::common::Mentions> operator()(const mtx::events::Event<T> &e)
+    {
+        if constexpr (requires { T::mentions; }) {
+            return e.content.mentions;
+        }
+        return std::nullopt;
+    }
+};
+
 struct SetEventRelations
 {
     mtx::common::Relations new_relations;
@@ -447,6 +459,11 @@ mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event)
 {
     return std::visit(EventRelations{}, event);
 }
+std::optional<mtx::common::Mentions>
+mtx::accessors::mentions(const mtx::events::collections::TimelineEvents &event)
+{
+    return std::visit(EventMentions{}, event);
+}
 
 void
 mtx::accessors::set_relations(mtx::events::collections::TimelineEvents &event,
diff --git a/src/EventAccessors.h b/src/EventAccessors.h
index 4128f681..3651e941 100644
--- a/src/EventAccessors.h
+++ b/src/EventAccessors.h
@@ -107,6 +107,8 @@ std::string
 mimetype(const mtx::events::collections::TimelineEvents &event);
 const mtx::common::Relations &
 relations(const mtx::events::collections::TimelineEvents &event);
+std::optional<mtx::common::Mentions>
+mentions(const mtx::events::collections::TimelineEvents &event);
 void
 set_relations(mtx::events::collections::TimelineEvents &event, mtx::common::Relations relations);
 std::string
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 62d38cf5..f8b57b81 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -179,31 +179,74 @@ InputBar::insertMimeData(const QMimeData *md)
 }
 
 void
-InputBar::updateTextContentProperties(const QString &t)
+InputBar::addMention(QString mention, QString text)
 {
-    // check for @room
-    bool roomMention = false;
-
-    if (t.size() > 4) {
-        QTextBoundaryFinder finder(QTextBoundaryFinder::BoundaryType::Word, t);
-
-        finder.toStart();
-        do {
-            auto start = finder.position();
-            finder.toNextBoundary();
-            auto end = finder.position();
-            if (start > 0 && end - start >= 4 &&
-                t.mid(start, end - start) == QLatin1String("room") &&
-                t.at(start - 1) == QChar('@')) {
-                roomMention = true;
-                break;
+    if (!mentions_.contains(mention)) {
+        mentions_.push_back(mention);
+        mentionTexts_.push_back(text);
+
+        emit mentionsChanged();
+        if (mention == u"@room") {
+            this->containsAtRoom_ = true;
+        }
+    }
+}
+
+void
+InputBar::removeMention(QString mention)
+{
+    if (auto idx = mentions_.indexOf(mention); idx != -1) {
+        mentions_.removeAt(idx);
+        mentionTexts_.removeAt(idx);
+        emit mentionsChanged();
+        if (mention == u"@room") {
+            this->containsAtRoom_ = false;
+        }
+    }
+}
+
+void
+InputBar::updateTextContentProperties(const QString &t, bool charDeleted)
+{
+    auto containsRoomMention = [](QStringView text) {
+        // check for @room
+        bool roomMention = false;
+        if (text.size() > 4) {
+            QTextBoundaryFinder finder(QTextBoundaryFinder::BoundaryType::Word, text);
+
+            finder.toStart();
+            do {
+                auto start = finder.position();
+                finder.toNextBoundary();
+                auto end = finder.position();
+                if (start > 0 && end - start >= 4 &&
+                    text.mid(start, end - start) == QStringView(u"room") &&
+                    text.at(start - 1) == QChar('@')) {
+                    roomMention = true;
+                    break;
+                }
+            } while (finder.position() < text.size());
+        }
+        return roomMention;
+    };
+
+    if (charDeleted) {
+        for (qsizetype idx = 0; idx < mentions_.size();) {
+            if (!t.contains(mentionTexts_.at(idx))) {
+                removeMention(mentions_.at(idx));
+            } else {
+                idx++;
             }
-        } while (finder.position() < t.size());
+        }
     }
 
+    auto roomMention = containsRoomMention(t);
+
     if (roomMention != this->containsAtRoom_) {
-        this->containsAtRoom_ = roomMention;
-        emit containsAtRoomChanged();
+        if (roomMention)
+            addMention(QStringLiteral(u"@room"), QStringLiteral(u"@room"));
+        else
+            removeMention(QStringLiteral(u"@room"));
     }
 
     // check for invalid commands
@@ -280,7 +323,7 @@ InputBar::setText(const QString &newText)
     if (history_.size() == INPUT_HISTORY_SIZE)
         history_.pop_back();
 
-    updateTextContentProperties(QLatin1String(""));
+    updateTextContentProperties(newText, true);
     emit textChanged(newText);
 }
 void
@@ -294,14 +337,15 @@ InputBar::updateState(int selectionStart_,
     else
         startTyping();
 
-    if (text_ != text()) {
+    auto oldText = text();
+    if (text_ != oldText) {
         if (history_.empty())
             history_.push_front(text_);
         else
             history_.front() = text_;
         history_index_ = 0;
 
-        updateTextContentProperties(text_);
+        updateTextContentProperties(text_, text_.size() < oldText.size());
         // disabled, as it moves the cursor to the end
         // emit textChanged(text_);
     }
@@ -452,6 +496,24 @@ InputBar::generateRelations() const
     return relations;
 }
 
+mtx::common::Mentions
+InputBar::generateMentions()
+{
+    std::vector<std::string> userMentions;
+    for (const auto &m : mentions_)
+        if (m != u"@room")
+            userMentions.push_back(m.toStdString());
+    auto mention = mtx::common::Mentions{
+      .user_ids = userMentions,
+      .room     = containsAtRoom_,
+    };
+
+    // this->containsAtRoom_ = false;
+    // this->mentions_.clear();
+    // this->mentionTexts_.clear();
+    return mention;
+}
+
 void
 InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbowify)
 {
@@ -484,6 +546,7 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow
         text.format = "org.matrix.custom.html";
     }
 
+    text.mentions  = generateMentions();
     text.relations = generateRelations();
     if (!room->reply().isEmpty() && room->thread().isEmpty() && room->edit().isEmpty()) {
         auto related = room->relatedInfo(room->reply());
@@ -540,6 +603,7 @@ InputBar::emote(const QString &msg, bool rainbowify)
         emote.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
     }
 
+    emote.mentions  = generateMentions();
     emote.relations = generateRelations();
 
     room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
@@ -560,6 +624,7 @@ InputBar::notice(const QString &msg, bool rainbowify)
         notice.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
     }
 
+    notice.mentions  = generateMentions();
     notice.relations = generateRelations();
 
     room->sendMessageEvent(notice, mtx::events::EventType::RoomMessage);
@@ -582,6 +647,7 @@ InputBar::confetti(const QString &body, bool rainbowify)
         confetti.body = replaceMatrixToMarkdownLink(body.trimmed()).toStdString();
     }
 
+    confetti.mentions  = generateMentions();
     confetti.relations = generateRelations();
 
     room->sendMessageEvent(confetti, mtx::events::EventType::RoomMessage);
@@ -606,6 +672,7 @@ InputBar::rainfall(const QString &body)
         rain.body = replaceMatrixToMarkdownLink(body.trimmed()).toStdString();
     }
 
+    rain.mentions  = generateMentions();
     rain.relations = generateRelations();
 
     room->sendMessageEvent(rain, mtx::events::EventType::RoomMessage);
@@ -630,6 +697,7 @@ InputBar::customMsgtype(const QString &msgtype, const QString &body)
         msg.body = replaceMatrixToMarkdownLink(body.trimmed()).toStdString();
     }
 
+    msg.mentions  = generateMentions();
     msg.relations = generateRelations();
 
     room->sendMessageEvent(msg, mtx::events::EventType::RoomMessage);
@@ -673,6 +741,7 @@ InputBar::image(const QString &filename,
         image.info.thumbnail_info.mimetype = "image/png";
     }
 
+    image.mentions  = generateMentions();
     image.relations = generateRelations();
 
     room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
@@ -695,6 +764,7 @@ InputBar::file(const QString &filename,
     else
         file.url = url.toStdString();
 
+    file.mentions  = generateMentions();
     file.relations = generateRelations();
 
     room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
@@ -722,6 +792,7 @@ InputBar::audio(const QString &filename,
     else
         audio.url = url.toStdString();
 
+    audio.mentions  = generateMentions();
     audio.relations = generateRelations();
 
     room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
@@ -771,6 +842,7 @@ InputBar::video(const QString &filename,
         video.info.thumbnail_info.mimetype = "image/png";
     }
 
+    video.mentions  = generateMentions();
     video.relations = generateRelations();
 
     room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
@@ -825,6 +897,7 @@ InputBar::sticker(QStringList descriptor)
             sticker.info.thumbnail_info.h        = sticker.info.h;
             sticker.info.thumbnail_info.w        = sticker.info.w;
 
+            sticker.mentions  = generateMentions();
             sticker.relations = generateRelations();
 
             room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index fbf08343..c38de662 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -158,12 +158,12 @@ class InputBar final : public QObject
 {
     Q_OBJECT
     Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
-    Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged)
     Q_PROPERTY(
       bool containsInvalidCommand READ containsInvalidCommand NOTIFY containsInvalidCommandChanged)
     Q_PROPERTY(bool containsIncompleteCommand READ containsIncompleteCommand NOTIFY
                  containsIncompleteCommandChanged)
     Q_PROPERTY(QString currentCommand READ currentCommand NOTIFY currentCommandChanged)
+    Q_PROPERTY(QStringList mentions READ mentions NOTIFY mentionsChanged)
     Q_PROPERTY(QString text READ text NOTIFY textChanged)
     Q_PROPERTY(QVariantList uploads READ uploads NOTIFY uploadsChanged)
 
@@ -188,7 +188,37 @@ public slots:
     QString nextText();
     void setText(const QString &newText);
 
-    [[nodiscard]] bool containsAtRoom() const { return containsAtRoom_; }
+    [[nodiscard]] QStringList mentions() const { return mentions_; }
+    void addMention(QString m, QString text);
+    void removeMention(QString m);
+
+    void storeForEdit()
+    {
+        textBeforeEdit     = text();
+        mentionsBefore     = mentions_;
+        mentionTextsBefore = mentionTexts_;
+        emit mentionsChanged();
+    }
+    void restoreAfterEdit()
+    {
+        mentions_     = mentionsBefore;
+        mentionTexts_ = mentionTextsBefore;
+        mentionsBefore.clear();
+        mentionTextsBefore.clear();
+        setText(textBeforeEdit);
+        textBeforeEdit.clear();
+        emit mentionsChanged();
+    }
+    void replaceMentions(QStringList newMentions, QStringList newMentionTexts)
+    {
+        if (newMentions.size() != newMentionTexts.size())
+            return;
+
+        mentions_     = newMentions;
+        mentionTexts_ = newMentionTexts;
+        emit mentionsChanged();
+    }
+
     bool containsInvalidCommand() const { return containsInvalidCommand_; }
     bool containsIncompleteCommand() const { return containsIncompleteCommand_; }
     QString currentCommand() const { return currentCommand_; }
@@ -218,8 +248,8 @@ private slots:
 signals:
     void textChanged(QString newText);
     void uploadingChanged(bool value);
-    void containsAtRoomChanged();
     void containsInvalidCommandChanged();
+    void mentionsChanged();
     void containsIncompleteCommandChanged();
     void currentCommandChanged();
     void uploadsChanged();
@@ -269,6 +299,7 @@ private:
     QPair<QString, QString> getCommandAndArgs() const { return getCommandAndArgs(text()); }
     QPair<QString, QString> getCommandAndArgs(const QString &currentText) const;
     mtx::common::Relations generateRelations() const;
+    mtx::common::Mentions generateMentions();
 
     void startUploadFromPath(const QString &path);
     void startUploadFromMimeData(const QMimeData &source, const QString &format);
@@ -281,7 +312,7 @@ private:
         }
     }
 
-    void updateTextContentProperties(const QString &t);
+    void updateTextContentProperties(const QString &t, bool textDeleted = false);
 
     void toggleIgnore(const QString &user, const bool ignored);
 
@@ -296,6 +327,10 @@ private:
     bool containsInvalidCommand_    = false;
     bool containsIncompleteCommand_ = false;
     QString currentCommand_;
+    QStringList mentions_, mentionTexts_;
+    // store stuff during edits
+    QStringList mentionsBefore, mentionTextsBefore;
+    QString textBeforeEdit;
 
     using UploadHandle = std::unique_ptr<MediaUpload, DeleteLaterDeleter>;
     std::vector<UploadHandle> unconfirmedUploads;
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 05a3c45c..e7fb31f5 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -3069,9 +3069,7 @@ TimelineModel::setEdit(const QString &newEdit)
     }
 
     if (edit_.isEmpty()) {
-        this->textBeforeEdit  = input()->text();
-        this->replyBeforeEdit = reply_;
-        nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString());
+        input()->storeForEdit();
     }
 
     auto quoted = [](QString in) { return in.replace("[", "\\[").replace("]", "\\]"); };
@@ -3083,6 +3081,24 @@ TimelineModel::setEdit(const QString &newEdit)
             setReply(QString::fromStdString(mtx::accessors::relations(e).reply_to().value_or("")));
             setThread(QString::fromStdString(mtx::accessors::relations(e).thread().value_or("")));
 
+            auto mentionsList = mtx::accessors::mentions(e);
+            QStringList mentions, mentionTexts;
+            if (mentionsList) {
+                if (mentionsList->room) {
+                    mentions.append(QStringLiteral(u"@room"));
+                    mentionTexts.append(QStringLiteral(u"@room"));
+                }
+
+                for (const auto &user : mentionsList->user_ids) {
+                    auto userid = QString::fromStdString(user);
+                    mentions.append(userid);
+                    mentionTexts.append(
+                      QStringLiteral("[%1](https://matrix.to/#/%2)")
+                        .arg(displayName(userid).replace("[", "\\[").replace("]", "\\]"),
+                             QString(QUrl::toPercentEncoding(userid))));
+                }
+            }
+
             auto msgType = mtx::accessors::msg_type(e);
             if (msgType == mtx::events::MessageType::Text ||
                 msgType == mtx::events::MessageType::Notice ||
@@ -3130,6 +3146,7 @@ TimelineModel::setEdit(const QString &newEdit)
             } else {
                 input()->setText(QLatin1String(""));
             }
+            input()->replaceMentions(std::move(mentions), std::move(mentionTexts));
 
             edit_ = newEdit;
         } else {
@@ -3148,9 +3165,7 @@ TimelineModel::resetEdit()
     if (!edit_.isEmpty()) {
         edit_ = QLatin1String("");
         emit editChanged(edit_);
-        nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString());
-        input()->setText(textBeforeEdit);
-        textBeforeEdit.clear();
+        input()->restoreAfterEdit();
         if (replyBeforeEdit.isEmpty())
             resetReply();
         else
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 08c776f8..c7f3ebb6 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -529,7 +529,7 @@ private:
 
     QString currentId, currentReadId;
     QString reply_, edit_, thread_;
-    QString textBeforeEdit, replyBeforeEdit;
+    QString replyBeforeEdit;
     QStringList typingUsers_;
 
     TimelineViewManager *manager_;