diff --git a/CMakeLists.txt b/CMakeLists.txt
index db77c1f7..aff561c9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -359,6 +359,7 @@ set(SRC_FILES
src/TrayIcon.cpp
src/UserSettingsPage.cpp
src/UsersModel.cpp
+ src/RoomDirectoryModel.cpp
src/RoomsModel.cpp
src/Utils.cpp
src/WebRTCSession.cpp
@@ -564,6 +565,8 @@ qt5_wrap_cpp(MOC_HEADERS
src/TrayIcon.h
src/UserSettingsPage.h
src/UsersModel.h
+ src/RoomDirectoryModel.h
+ src/RoomsModel.h
src/WebRTCSession.h
src/WelcomePage.h
src/ReadReceiptsModel.h
diff --git a/resources/qml/RoomDirectory.qml b/resources/qml/RoomDirectory.qml
new file mode 100644
index 00000000..4db24f01
--- /dev/null
+++ b/resources/qml/RoomDirectory.qml
@@ -0,0 +1,204 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "./ui"
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+ id: roomDirectoryWindow
+
+ property RoomDirectoryModel publicRooms
+
+ visible: true
+ minimumWidth: 650
+ minimumHeight: 420
+ palette: Nheko.colors
+ color: Nheko.colors.window
+ modality: Qt.WindowModal
+ flags: Qt.Dialog | Qt.WindowCloseButtonHint
+ Component.onCompleted: Nheko.reparent(roomDirectoryWindow)
+ title: qsTr("Explore Public Rooms")
+
+ Shortcut {
+ sequence: StandardKey.Cancel
+ onActivated: roomDirectoryWindow.close()
+ }
+
+ ListView {
+ id: roomDirView
+
+ anchors.fill: parent
+ model: publicRooms
+
+ ScrollHelper {
+ flickable: parent
+ anchors.fill: parent
+ enabled: !Settings.mobileMode
+ }
+
+ delegate: Rectangle {
+ id: roomDirDelegate
+
+ property color background: Nheko.colors.window
+ property color importantText: Nheko.colors.text
+ property color unimportantText: Nheko.colors.buttonText
+ property int avatarSize: fontMetrics.lineSpacing * 4
+
+ color: background
+ height: avatarSize + Nheko.paddingLarge
+ width: ListView.view.width
+
+ RowLayout {
+ spacing: Nheko.paddingMedium
+ anchors.fill: parent
+ anchors.margins: Nheko.paddingLarge
+ implicitHeight: textContent.height
+
+ Avatar {
+ id: roomAvatar
+
+ Layout.alignment: Qt.AlignVCenter
+ width: avatarSize
+ height: avatarSize
+ url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+ displayName: model.name
+ }
+
+ ColumnLayout {
+ id: textContent
+
+ Layout.alignment: Qt.AlignLeft
+ width: parent.width - avatar.width
+ Layout.preferredWidth: parent.width - avatar.width
+ spacing: Nheko.paddingSmall
+
+ ElidedLabel {
+ Layout.alignment: Qt.AlignBottom
+ color: roomDirDelegate.importantText
+ elideWidth: textContent.width - numMembersRectangle.width - buttonRectangle.width
+ font.pixelSize: fontMetrics.font.pixelSize * 1.1
+ fullText: model.name
+ }
+
+ RowLayout {
+ id: roomDescriptionRow
+
+ Layout.preferredWidth: parent.width
+ spacing: Nheko.paddingSmall
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+ Layout.preferredHeight: fontMetrics.lineSpacing * 4
+
+ Label {
+ id: roomTopic
+
+ color: roomDirDelegate.unimportantText
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+ font.pixelSize: fontMetrics.font.pixelSize
+ elide: Text.ElideRight
+ maximumLineCount: 2
+ Layout.fillWidth: true
+ text: model.topic
+ verticalAlignment: Text.AlignVCenter
+ wrapMode: Text.WordWrap
+ }
+
+ Item {
+ id: numMembersRectangle
+
+ Layout.margins: Nheko.paddingSmall
+ width: roomCount.width
+
+ Label {
+ id: roomCount
+
+ color: roomDirDelegate.unimportantText
+ anchors.centerIn: parent
+ font.pixelSize: fontMetrics.font.pixelSize
+ text: model.numMembers.toString()
+ }
+
+ }
+
+ Item {
+ id: buttonRectangle
+
+ Layout.margins: Nheko.paddingSmall
+ width: joinRoomButton.width
+
+ Button {
+ id: joinRoomButton
+
+ visible: publicRooms.canJoinRoom(model.roomid)
+ anchors.centerIn: parent
+ width: Math.ceil(0.1 * roomDirectoryWindow.width)
+ text: "Join"
+ onClicked: publicRooms.joinRoom(model.index)
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ footer: Item {
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width
+ visible: !publicRooms.reachedEndOfPagination && publicRooms.loadingMoreRooms
+ // hacky but works
+ height: loadingSpinner.height + 2 * Nheko.paddingLarge
+ anchors.margins: Nheko.paddingLarge
+
+ Spinner {
+ id: loadingSpinner
+
+ anchors.centerIn: parent
+ anchors.margins: Nheko.paddingLarge
+ running: visible
+ foreground: Nheko.colors.mid
+ }
+
+ }
+
+ }
+
+ publicRooms: RoomDirectoryModel {
+ }
+
+ header: RowLayout {
+ id: searchBarLayout
+
+ spacing: Nheko.paddingMedium
+ width: parent.width
+ implicitHeight: roomSearch.height
+
+ MatrixTextField {
+ id: roomSearch
+
+ Layout.fillWidth: true
+ selectByMouse: true
+ font.pixelSize: fontMetrics.font.pixelSize
+ padding: Nheko.paddingMedium
+ color: Nheko.colors.text
+ placeholderText: qsTr("Search for public rooms")
+ onTextChanged: searchTimer.restart()
+ }
+
+ Timer {
+ id: searchTimer
+
+ interval: 350
+ onTriggered: roomDirView.model.setSearchTerm(roomSearch.text)
+ }
+
+ }
+
+}
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 8fbfce91..b84b4c36 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -16,6 +16,13 @@ Page {
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
property bool collapsed: false
+Component {
+ id: roomDirectoryComponent
+
+ RoomDirectory {
+ }
+ }
+
ListView {
id: roomlist
@@ -563,6 +570,10 @@ Page {
ToolTip.visible: hovered
ToolTip.text: qsTr("Room directory")
Layout.margins: Nheko.paddingMedium
+ onClicked: {
+ var win = roomDirectoryComponent.createObject(timelineRoot);
+ win.show();
+ }
}
ImageButton {
diff --git a/resources/res.qrc b/resources/res.qrc
index f50265ca..b46b726c 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -142,6 +142,7 @@
qml/emoji/EmojiPicker.qml
qml/emoji/StickerPicker.qml
qml/UserProfile.qml
+ qml/RoomDirectory.qml
qml/delegates/MessageDelegate.qml
qml/delegates/Encrypted.qml
qml/delegates/FileMessage.qml
diff --git a/src/RoomDirectoryModel.cpp b/src/RoomDirectoryModel.cpp
new file mode 100644
index 00000000..61c3eb72
--- /dev/null
+++ b/src/RoomDirectoryModel.cpp
@@ -0,0 +1,194 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RoomDirectoryModel.h"
+#include "Cache.h"
+#include "ChatPage.h"
+
+#include
+
+RoomDirectoryModel::RoomDirectoryModel(QObject *parent, const std::string &s)
+ : QAbstractListModel(parent)
+ , server_(s)
+{
+ connect(this,
+ &RoomDirectoryModel::fetchedRoomsBatch,
+ this,
+ &RoomDirectoryModel::displayRooms,
+ Qt::QueuedConnection);
+}
+
+QHash
+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;
+
+ publicRoomsData_.clear();
+
+ 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();
+}
+
+bool
+RoomDirectoryModel::canJoinRoom(const QByteArray &room)
+{
+ const QString room_id(room);
+ return !room_id.isEmpty() && !cache::getRoomInfo({room_id.toStdString()}).count(room_id);
+}
+
+std::vector
+RoomDirectoryModel::getViasForRoom(const std::vector &aliases)
+{
+ std::vector vias;
+
+ vias.reserve(aliases.size());
+
+ std::transform(aliases.begin(),
+ aliases.end(),
+ std::back_inserter(vias),
+ [](const auto &alias) { return alias.substr(alias.find(":") + 1); });
+
+ return vias;
+}
+
+void
+RoomDirectoryModel::joinRoom(const int &index)
+{
+ if (index >= 0 && static_cast(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 &)
+{
+ if (!canFetchMore_)
+ return;
+
+ nhlog::net()->debug("Fetching more rooms from mtxclient...");
+
+ 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_;
+
+ reachedEndOfPagination_ = false;
+ emit reachedEndOfPaginationChanged();
+
+ loadingMoreRooms_ = true;
+ emit loadingMoreRoomsChanged();
+
+ http::client()->post_public_rooms(
+ req,
+ [requested_server, this, req](const mtx::responses::PublicRooms &res,
+ mtx::http::RequestErr err) {
+ loadingMoreRooms_ = false;
+ emit loadingMoreRoomsChanged();
+
+ 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.next_batch);
+ }
+ },
+ requested_server);
+}
+
+void
+RoomDirectoryModel::displayRooms(std::vector fetched_rooms,
+ const std::string &next_batch)
+{
+ nhlog::net()->debug("Prev batch: {} | Next batch: {}", prevBatch_, next_batch);
+
+ if (fetched_rooms.empty()) {
+ nhlog::net()->error("mtxclient helper thread yielded empty chunk!");
+ return;
+ }
+
+ beginInsertRows(QModelIndex(),
+ static_cast(publicRoomsData_.size()),
+ static_cast(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;
+ reachedEndOfPagination_ = true;
+ emit reachedEndOfPaginationChanged();
+ }
+
+ prevBatch_ = next_batch;
+
+ nhlog::ui()->debug("Finished loading rooms");
+}
diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h
new file mode 100644
index 00000000..791384fa
--- /dev/null
+++ b/src/RoomDirectoryModel.h
@@ -0,0 +1,96 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "MatrixClient.h"
+#include
+#include
+
+#include "Logging.h"
+
+namespace mtx::http {
+using RequestErr = const std::optional &;
+}
+namespace mtx::responses {
+struct PublicRooms;
+}
+
+class RoomDirectoryModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(bool loadingMoreRooms READ loadingMoreRooms NOTIFY loadingMoreRoomsChanged)
+ Q_PROPERTY(bool reachedEndOfPagination READ reachedEndOfPagination NOTIFY
+ reachedEndOfPaginationChanged)
+
+public:
+ explicit RoomDirectoryModel(QObject *parent = nullptr, const std::string &s = "");
+
+ enum Roles
+ {
+ Name = Qt::UserRole,
+ Id,
+ AvatarUrl,
+ Topic,
+ MemberCount,
+ Previewable
+ };
+ QHash 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(publicRoomsData_.size());
+ }
+
+ bool canFetchMore(const QModelIndex &) const override { return canFetchMore_; }
+
+ bool loadingMoreRooms() const { return loadingMoreRooms_; }
+
+ bool reachedEndOfPagination() const { return reachedEndOfPagination_; }
+
+ void fetchMore(const QModelIndex &) override;
+
+ Q_INVOKABLE bool canJoinRoom(const QByteArray &room);
+ Q_INVOKABLE void joinRoom(const int &index = -1);
+
+signals:
+ void fetchedRoomsBatch(std::vector rooms,
+ const std::string &next_batch);
+ void loadingMoreRoomsChanged();
+ void reachedEndOfPaginationChanged();
+
+public slots:
+ void setMatrixServer(const QString &s = "");
+ void setSearchTerm(const QString &f);
+
+private slots:
+
+ void displayRooms(std::vector rooms,
+ const std::string &next_batch);
+
+private:
+ static constexpr size_t limit_ = 50;
+
+ std::string server_;
+ std::string userSearchString_;
+ std::string prevBatch_;
+ std::string nextBatch_;
+ bool canFetchMore_{true};
+ bool loadingMoreRooms_{false};
+ bool reachedEndOfPagination_{false};
+ std::vector publicRoomsData_;
+
+ std::vector getViasForRoom(const std::vector &room);
+ void resetDisplayedData();
+};
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 906e328f..97b60b0c 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -27,6 +27,7 @@
#include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "ReadReceiptsModel.h"
+#include "RoomDirectoryModel.h"
#include "RoomsModel.h"
#include "SingleImagePackModel.h"
#include "UserSettingsPage.h"
@@ -40,6 +41,7 @@
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector)
+Q_DECLARE_METATYPE(std::vector)
namespace msgs = mtx::events::msg;
@@ -151,6 +153,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qRegisterMetaType();
qRegisterMetaType();
+ qRegisterMetaType>();
+
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
1,
@@ -282,6 +286,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"EmojiCategory",
"Error: Only enums");
+ qmlRegisterType("im.nheko", 1, 0, "RoomDirectoryModel");
+
#ifdef USE_QUICK_VIEW
view = new QQuickView(parent);
container = QWidget::createWindowContainer(view, parent);