diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 50ff7324947bd8a161373f8ed881f324d025eacb..d9592e1439650aceed5ca63c1d6b500e382ad939 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -128,6 +128,9 @@ Rectangle {
                         textArea.clear();
                         event.accepted = true;
                     }
+                    if (popup.opened)
+                        popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition));
+
                 }
 
                 Connections {
diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h
index 757aa9902d194757fd16c27a9c07c977e3bbbabb..ee8236e36c5181c93d077fc57222f7a3cb07fc00 100644
--- a/src/CompletionProxyModel.h
+++ b/src/CompletionProxyModel.h
@@ -5,6 +5,7 @@
 #include <QSortFilterProxyModel>
 
 #include "CompletionModelRoles.h"
+#include "Utils.h"
 
 class CompletionProxyModel : public QSortFilterProxyModel
 {
@@ -15,6 +16,20 @@ public:
           : QSortFilterProxyModel(parent)
         {
                 setSourceModel(model);
+                sort(0, Qt::AscendingOrder);
+                setFilterRole(CompletionModel::SearchRole);
+
+                connect(
+                  this,
+                  &CompletionProxyModel::newSearchString,
+                  this,
+                  [this](QString s) {
+                          s.remove(":");
+                          s.remove("@");
+                          searchString = s.toLower();
+                          invalidate();
+                  },
+                  Qt::QueuedConnection);
         }
 
         QHash<int, QByteArray> roleNames() const override
@@ -28,6 +43,79 @@ public:
                 return (row_count < 7) ? row_count : 7;
         }
 
+        bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+        {
+                if (searchString.size() < 1)
+                        return true;
+
+                auto source_index = sourceModel()->index(source_row, 0, source_parent);
+                auto role1        = sourceModel()
+                               ->data(source_index, CompletionModel::SearchRole)
+                               .toString()
+                               .toLower();
+
+                if (role1.contains(searchString))
+                        return true;
+                // auto score =
+                //  utils::levenshtein_distance(searchString, role1.toLower().toStdString());
+                // if ((size_t)role1.size() >= searchString.size() &&
+                //    ((size_t)score) < (size_t)role1.size() - searchString.size() + 2)
+                //        return true;
+
+                auto role2 = sourceModel()
+                               ->data(source_index, CompletionModel::SearchRole2)
+                               .toString()
+                               .toLower();
+                if (role2.contains(searchString))
+                        return true;
+                // if (!role2.isEmpty()) {
+                //        score =
+                //          utils::levenshtein_distance(searchString,
+                //          role2.toLower().toStdString());
+                //        if ((size_t)role2.size() >= searchString.size() &&
+                //            ((size_t)score) < (size_t)role2.size() - searchString.size() + 2)
+                //                return true;
+                //}
+
+                return false;
+        }
+
+        bool lessThan(const QModelIndex &source_left,
+                      const QModelIndex &source_right) const override
+        {
+                if (searchString.size() < 1)
+                        return false;
+
+                auto left1 =
+                  sourceModel()->data(source_left, CompletionModel::SearchRole).toString();
+                auto left2 =
+                  sourceModel()->data(source_left, CompletionModel::SearchRole2).toString();
+                auto left = left1.toLower().indexOf(searchString);
+                // utils::levenshtein_distance(searchString, left1.toLower().toStdString());
+                if (!left2.isEmpty()) {
+                        // left = std::min(
+                        //  utils::levenshtein_distance(searchString,
+                        //  left2.toLower().toStdString()), left);
+                        left = std::min(left2.toLower().indexOf(searchString), left);
+                }
+
+                auto right1 =
+                  sourceModel()->data(source_right, CompletionModel::SearchRole).toString();
+                auto right2 =
+                  sourceModel()->data(source_right, CompletionModel::SearchRole2).toString();
+                auto right = right1.toLower().indexOf(searchString);
+                // auto right =
+                //  utils::levenshtein_distance(searchString, right1.toLower().toStdString());
+                if (!right2.isEmpty()) {
+                        // right = std::min(
+                        //  utils::levenshtein_distance(searchString,
+                        //  right2.toLower().toStdString()), right);
+                        right = std::min(right2.toLower().indexOf(searchString), right);
+                }
+
+                return left < right;
+        }
+
 public slots:
         QVariant completionAt(int i) const
         {
@@ -36,4 +124,12 @@ public slots:
                 else
                         return {};
         }
+
+        void setSearchString(QString s) { emit newSearchString(s); }
+
+signals:
+        void newSearchString(QString);
+
+private:
+        QString searchString;
 };
diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp
index f102cff152ebbe1f319931ad44e0620666f300ba..cb9f8f75e73d0e7146854d9c99aeaeb95b8ce764 100644
--- a/src/UsersModel.cpp
+++ b/src/UsersModel.cpp
@@ -8,6 +8,10 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent)
   , room_id(roomId)
 {
         roomMembers_ = cache::roomMembers(roomId);
+        for (const auto &m : roomMembers_) {
+                displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m)));
+                userids.push_back(QString::fromStdString(m));
+        }
 }
 
 QHash<int, QByteArray>
@@ -31,10 +35,9 @@ UsersModel::data(const QModelIndex &index, int role) const
                 case CompletionModel::SearchRole:
                 case Qt::DisplayRole:
                 case Roles::DisplayName:
-                        return QString::fromStdString(
-                          cache::displayName(room_id, roomMembers_[index.row()]));
+                        return displayNames[index.row()];
                 case CompletionModel::SearchRole2:
-                        return QString::fromStdString(roomMembers_[index.row()]);
+                        return userids[index.row()];
                 case Roles::AvatarUrl:
                         return cache::avatarUrl(QString::fromStdString(room_id),
                                                 QString::fromStdString(roomMembers_[index.row()]));
diff --git a/src/UsersModel.h b/src/UsersModel.h
index 6ee8261f607b9acfa5fa728940b6a6ae7a5a8a87..cddcdd84c56e1447945edaa38e8df37486c515b0 100644
--- a/src/UsersModel.h
+++ b/src/UsersModel.h
@@ -23,4 +23,6 @@ public:
 private:
         std::string room_id;
         std::vector<std::string> roomMembers_;
+        std::vector<QString> displayNames;
+        std::vector<QString> userids;
 };