diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7b26602cfc0c02b1ad7d70d129fe1976a86e3a74..587059fe68e0ccf3fbfab2082d248a4920d51a20 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -361,6 +361,7 @@ set(SRC_FILES
 	src/UserSettingsPage.cpp
 	src/UsersModel.cpp
 	src/RoomsModel.cpp
+	src/RoomDirectoryModel.cpp
 	src/Utils.cpp
 	src/WebRTCSession.cpp
 	src/WelcomePage.cpp
@@ -567,6 +568,7 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/UserSettingsPage.h
 	src/UsersModel.h
 	src/RoomsModel.h
+	src/RoomDirectoryModel.h
 	src/WebRTCSession.h
 	src/WelcomePage.h
 	)
diff --git a/src/RoomDirectoryModel.cpp b/src/RoomDirectoryModel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..397871eb566b3e8d34fa7bf8f6ad6136ff51c711
--- /dev/null
+++ b/src/RoomDirectoryModel.cpp
@@ -0,0 +1,171 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RoomDirectoryModel.h"
+#include "ChatPage.h"
+
+#include <algorithm>
+
+RoomDirectoryModel::RoomDirectoryModel(QObject *parent, const std::string &s)
+                    : QAbstractListModel(parent)
+                    , server_(s)
+                    , canFetchMore_(true)
+{
+    connect(this, &RoomDirectoryModel::fetchedRoomsBatch, this, &RoomDirectoryModel::displayRooms, Qt::QueuedConnection);    
+}
+
+QHash<int, QByteArray> 
+RoomDirectoryModel::roleNames() const
+{
+    return {
+        {Roles::Name, "name"},
+        {Roles::Id, "roomid"},
+        {Roles::AvatarUrl, "avatarUrl"},
+        {Roles::Topic, "topic"},
+        {Roles::MemberCount, "numMembers"},
+        {Roles::Previewable, "canPreview"},};
+}
+
+void
+RoomDirectoryModel::resetDisplayedData()
+{
+    beginResetModel();
+
+    prevBatch_ = "";
+    nextBatch_ = "";
+    canFetchMore_ = true;
+
+    beginRemoveRows(QModelIndex(), 0 , static_cast<int> (publicRoomsData_.size()));
+    publicRoomsData_.clear();
+    endRemoveRows();
+
+    endResetModel();
+}
+
+void
+RoomDirectoryModel::setMatrixServer(const QString &s)
+{
+    server_ = s.toStdString();
+
+    nhlog::ui()->debug("Received matrix server: {}", server_);
+
+    resetDisplayedData();
+}
+
+void
+RoomDirectoryModel::setSearchTerm(const QString &f)
+{
+    userSearchString_ = f.toStdString();
+
+    nhlog::ui()->debug("Received user query: {}", userSearchString_);
+
+    resetDisplayedData();
+}
+
+std::vector<std::string>
+RoomDirectoryModel::getViasForRoom(const std::vector<std::string> &aliases)
+{
+    std::vector<std::string> vias;
+    
+    vias.reserve(aliases.size());
+
+    std::transform(aliases.begin(), aliases.end(), 
+                   std::back_inserter(vias), [](const auto &alias) {
+                        const auto roomAliasDelimiter = ":";
+                        return alias.substr(alias.find(roomAliasDelimiter) + 1);
+                   });
+
+    return vias;
+}
+
+void
+RoomDirectoryModel::joinRoom(const int &index)
+{
+    if (index >= 0 && static_cast<size_t> (index) < publicRoomsData_.size())
+    {
+        const auto &chunk = publicRoomsData_[index];
+        nhlog::ui()->debug("'Joining room {}", chunk.room_id);
+        ChatPage::instance()->joinRoomVia(chunk.room_id, getViasForRoom(chunk.aliases));
+    }
+}
+
+QVariant
+RoomDirectoryModel::data(const QModelIndex &index, int role) const
+{
+    if (hasIndex(index.row(), index.column(), index.parent())) 
+    {
+        const auto &room_chunk = publicRoomsData_[index.row()]; 
+        switch (role)
+        {
+            case Roles::Name:
+                return QString::fromStdString(room_chunk.name); 
+            case Roles::Id:
+                return QString::fromStdString(room_chunk.room_id);
+            case Roles::AvatarUrl:
+                return QString::fromStdString(room_chunk.avatar_url);
+            case Roles::Topic:
+                return QString::fromStdString(room_chunk.topic);
+            case Roles::MemberCount:
+                return QVariant::fromValue(room_chunk.num_joined_members);
+            case Roles::Previewable:
+                return QVariant::fromValue(room_chunk.world_readable);
+        }
+    }
+    return {};
+}
+
+void
+RoomDirectoryModel::fetchMore(const QModelIndex &) 
+{
+    nhlog::net()->debug("Fetching more rooms from mtxclient...");
+    nhlog::net()->debug("Prev batch: {} | Next batch: {}", prevBatch_, nextBatch_);
+
+    mtx::requests::PublicRooms req;
+    req.limit = limit_;
+    req.since = prevBatch_;
+    req.filter.generic_search_term = userSearchString_;
+    // req.third_party_instance_id = third_party_instance_id;
+    auto requested_server = server_;
+
+    http::client()->post_public_rooms(req, [requested_server, this, req]
+                                            (const mtx::responses::PublicRooms &res, 
+                                            mtx::http::RequestErr err) 
+                                            {
+                                                if (err) {
+                                                    nhlog::net()->error
+                                                    ("Failed to retrieve rooms from mtxclient - {} - {} - {}",
+                                                    mtx::errors::to_string(err->matrix_error.errcode),
+                                                    err->matrix_error.error,
+                                                    err->parse_error);
+                                                } else if (   req.filter.generic_search_term == this->userSearchString_
+                                                           && req.since                      == this->prevBatch_
+                                                           && requested_server               == this->server_) {
+                                                    nhlog::net()->debug("signalling chunk to GUI thread");
+                                                    emit fetchedRoomsBatch(res.chunk, res.prev_batch, res.next_batch); 
+                                                }
+                                            }, requested_server);
+}
+
+void
+RoomDirectoryModel::displayRooms(std::vector<mtx::responses::PublicRoomsChunk> fetched_rooms,
+                                 const std::string &prev_batch, const std::string &next_batch)
+{
+    nhlog::net()->debug("Prev batch: {} | Next batch: {}", prevBatch_, nextBatch_);
+    nhlog::net()->debug("NP batch: {} | NN batch: {}", prev_batch, next_batch);
+
+    if (fetched_rooms.empty()) {
+        nhlog::net()->error("mtxclient helper thread yielded empty chunk!");
+        return;
+    }
+
+    beginInsertRows(QModelIndex(), static_cast<int> (publicRoomsData_.size()), static_cast<int> (publicRoomsData_.size() + fetched_rooms.size()) - 1);
+    this->publicRoomsData_.insert(this->publicRoomsData_.end(), fetched_rooms.begin(), fetched_rooms.end());
+    endInsertRows();
+
+    if (next_batch.empty()) {
+        canFetchMore_ = false;
+    }
+    
+    prevBatch_ = std::exchange(nextBatch_, next_batch);
+}
\ No newline at end of file
diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h
new file mode 100644
index 0000000000000000000000000000000000000000..125813dadaf6eefead51ad8dd398c9e3bfcc1f5c
--- /dev/null
+++ b/src/RoomDirectoryModel.h
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QHash>
+#include <QString>
+#include <vector>
+#include <string>
+
+#include "MatrixClient.h"
+#include <mtxclient/http/errors.hpp>
+#include <mtx/responses/public_rooms.hpp>
+
+#include "Logging.h"
+
+namespace mtx::http {
+using RequestErr = const std::optional<mtx::http::ClientError> &;
+}
+namespace mtx::responses {
+struct PublicRooms;
+}
+
+class RoomDirectoryModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+public: 
+    explicit RoomDirectoryModel
+    (QObject *parent = nullptr, const std::string &s = "");
+
+    enum Roles {
+        Name = Qt::UserRole,
+        Id,
+        AvatarUrl,
+        Topic,
+        MemberCount,
+        Previewable
+    };
+    QHash<int, QByteArray> roleNames() const override;
+
+    QVariant data(const QModelIndex &index, int role) const override;
+
+    inline int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+            (void) parent;
+            return static_cast<int> (publicRoomsData_.size());
+    }
+
+    inline bool canFetchMore(const QModelIndex &) const override
+    {
+        nhlog::net()->debug("determining if can fetch more");
+        return canFetchMore_;
+    }
+    void fetchMore(const QModelIndex &) override;
+
+    Q_INVOKABLE void joinRoom(const int &index = -1);
+
+signals:
+    void fetchedRoomsBatch(std::vector<mtx::responses::PublicRoomsChunk> rooms, 
+                                        const std::string &prev_batch, const std::string &next_batch);
+    void serverChanged();
+    void searchTermEntered();
+
+public slots:
+    void displayRooms(std::vector<mtx::responses::PublicRoomsChunk> rooms,
+                      const std::string &prev, const std::string &next);
+    void setMatrixServer(const QString &s = "");
+    void setSearchTerm(const QString &f);
+
+private:
+    static constexpr size_t limit_ = 50;
+    
+    std::string server_;
+    std::string userSearchString_;
+    std::string prevBatch_;
+    std::string nextBatch_;
+    bool canFetchMore_;
+    std::vector<mtx::responses::PublicRoomsChunk> publicRoomsData_;
+
+    std::vector<std::string> getViasForRoom(const std::vector<std::string> &room);
+    void resetDisplayedData();
+};
\ No newline at end of file
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index a6922be7a1d9417cd92ca5ff9e5f38fa4f6b2e25..2e6c45cac4081ec330001aa62d3476308fe2defa 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -26,6 +26,8 @@
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "MxcImageProvider.h"
+#include "ReadReceiptsModel.h"
+#include "RoomDirectoryModel.h"
 #include "RoomsModel.h"
 #include "SingleImagePackModel.h"
 #include "UserSettingsPage.h"
@@ -39,6 +41,7 @@
 
 Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
 Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
+Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
 
 namespace msgs = mtx::events::msg;
 
@@ -150,6 +153,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
         qRegisterMetaType<CombinedImagePackModel *>();
 
+	qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>();
+
         qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
                                          "im.nheko",
                                          1,
@@ -273,6 +278,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                                          "EmojiCategory",
                                          "Error: Only enums");
 
+	qmlRegisterType<RoomDirectoryModel>(
+          "im.nheko.RoomDirectoryModel", 1, 0, "RoomDirectoryModel");
+
 #ifdef USE_QUICK_VIEW
         view      = new QQuickView(parent);
         container = QWidget::createWindowContainer(view, parent);