diff --git a/src/Config.h b/src/Config.h
index 72521f2088811aa6385fd9749748adef175c701f..5a742337d6714688128becabedba855563b6e527 100644
--- a/src/Config.h
+++ b/src/Config.h
@@ -30,9 +30,7 @@ const QRegularExpression url_regex(
   //          vvvv atomic match url -> fail if there is a " before or after        vvv
   QStringLiteral(
     R"((?<!["'])(?>((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!["']))"));
-// match any markdown matrix.to link. Capture group 1 is the link name, group 2 is the target.
-static const QRegularExpression
-  matrixToMarkdownLink(QStringLiteral(R"(\[(.*?)(?<!\\)\]\((https://matrix.to/#/.*?\)))"));
+// A matrix link to be converted back to markdown
 static const QRegularExpression
   matrixToLink(QStringLiteral(R"(<a href=\"(https://matrix.to/#/.*?)\">(.*?)</a>)"));
 }
diff --git a/src/RoomsModel.cpp b/src/RoomsModel.cpp
index 68cfaf1b71229feb91ff67ee3a8c0906d47a773c..8abcb32ec79e5073eec270e71395a4aa819b3449 100644
--- a/src/RoomsModel.cpp
+++ b/src/RoomsModel.cpp
@@ -61,7 +61,11 @@ RoomsModel::data(const QModelIndex &index, int role) const
             if (UserSettings::instance()->markdown()) {
                 QString percentEncoding = QUrl::toPercentEncoding(roomAliases[index.row()]);
                 return QStringLiteral("[%1](https://matrix.to/#/%2)")
-                  .arg(roomAliases[index.row()], percentEncoding);
+                  .arg(QString(roomAliases[index.row()])
+                         .replace("[", "\\[")
+                         .replace("]", "\\]")
+                         .toHtmlEscaped(),
+                       percentEncoding);
             } else {
                 return roomAliases[index.row()];
             }
diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp
index ecd76cf43b30202a1d0d5363bf72b6fefaf44e24..5d7dd5b78ba79aee02cffdec91f8d1a0765a4318 100644
--- a/src/UsersModel.cpp
+++ b/src/UsersModel.cpp
@@ -43,7 +43,10 @@ UsersModel::data(const QModelIndex &index, int role) const
         case CompletionModel::CompletionRole:
             if (UserSettings::instance()->markdown())
                 return QStringLiteral("[%1](https://matrix.to/#/%2)")
-                  .arg(displayNames[index.row()].toHtmlEscaped(),
+                  .arg(QString(displayNames[index.row()])
+                         .replace("[", "\\[")
+                         .replace("]", "\\]")
+                         .toHtmlEscaped(),
                        QString(QUrl::toPercentEncoding(userids[index.row()])));
             else
                 return displayNames[index.row()];
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 66bc8ef9c5ed29f035cd13995935103e290ef4b1..47efa867013bf369484510856333fa91a6e17863 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -343,6 +343,47 @@ InputBar::openFileSelection()
     startUploadFromPath(fileName);
 }
 
+QString
+replaceMatrixToMarkdownLink(QString input)
+{
+    bool replaced = false;
+    do {
+        replaced = false;
+
+        int endOfName = input.indexOf("](https://matrix.to/#/");
+        int startOfName;
+        int nestingCount = 0;
+        for (startOfName = endOfName - 1; startOfName > 0; startOfName--) {
+            // skip escaped chars
+            if (startOfName > 0 && input[startOfName - 1] == '\\')
+                continue;
+
+            if (input[startOfName] == '[') {
+                if (nestingCount <= 0)
+                    break;
+                else
+                    nestingCount--;
+            }
+            if (input[startOfName] == ']')
+                nestingCount++;
+        }
+        if (startOfName < 0 || nestingCount > 0)
+            break;
+
+        int endOfLink = input.indexOf(')', endOfName);
+        int newline   = input.indexOf('\n', endOfName);
+        if (endOfLink > endOfName && (newline == -1 || endOfLink < newline)) {
+            auto name = input.mid(startOfName + 1, endOfName - startOfName - 1);
+            name.replace("\\[", "[");
+            name.replace("\\]", "]");
+            input.replace(startOfName, endOfLink - startOfName + 1, name);
+            replaced = true;
+        }
+    } while (replaced);
+
+    return input;
+}
+
 void
 InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbowify)
 {
@@ -354,7 +395,7 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow
         useMarkdown == MarkdownOverride::ON) {
         text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString();
         // Remove markdown links by completer
-        text.body = msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
+        text.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
 
         // Don't send formatted_body, when we don't need to
         if (text.formatted_body.find('<') == std::string::npos)
@@ -392,7 +433,8 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow
                 }
             }
 
-            text.body = QStringLiteral("%1\n%2").arg(body, msg).toStdString();
+            text.body =
+              QStringLiteral("%1\n%2").arg(body, QString::fromStdString(text.body)).toStdString();
 
             // NOTE(Nico): rich replies always need a formatted_body!
             text.format = "org.matrix.custom.html";
@@ -426,8 +468,7 @@ InputBar::emote(const QString &msg, bool rainbowify)
         emote.formatted_body = html.toStdString();
         emote.format         = "org.matrix.custom.html";
         // Remove markdown links by completer
-        emote.body =
-          msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
+        emote.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
     }
 
     if (!room->reply().isEmpty()) {
@@ -454,8 +495,7 @@ InputBar::notice(const QString &msg, bool rainbowify)
         notice.formatted_body = html.toStdString();
         notice.format         = "org.matrix.custom.html";
         // Remove markdown links by completer
-        notice.body =
-          msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
+        notice.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
     }
 
     if (!room->reply().isEmpty()) {
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index c60940a72c2e0c38f155535b568b3252abe8aa60..b2798e26ae85415dd8e075d8a287c358e157ca2d 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -2625,6 +2625,8 @@ TimelineModel::setEdit(const QString &newEdit)
         nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString());
     }
 
+    auto quoted = [](QString in) { return in.replace("[", "\\[").replace("]", "\\]"); };
+
     if (edit_ != newEdit) {
         auto ev = events.get(newEdit.toStdString(), "");
         if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
@@ -2649,7 +2651,7 @@ TimelineModel::setEdit(const QString &newEdit)
 
                     for (const auto &[user, link] : reverseNameMapping) {
                         // TODO(Nico): html unescape the user name
-                        editText.replace(user, QStringLiteral("[%1](%2)").arg(user, link));
+                        editText.replace(user, QStringLiteral("[%1](%2)").arg(quoted(user), link));
                     }
                 }